Faculty Bot’s user management system handles user authentication, email verification, and account switching. Users are identified by their Discord ID and can link multiple email addresses.
User Structure
Users are represented by the User struct:
#[derive( Serialize , Deserialize , Debug )]
pub struct User {
id : u64 ,
role : Roles ,
}
User Properties
id : Discord user ID (u64)
role : Permission level (Unprivileged, User, Moderator, Admin)
User Creation
Users are created after successful Discord OAuth:
impl User {
pub fn new ( id : u64 ) -> Self {
User {
id ,
role : Roles :: Unprivileged ,
}
}
}
New users start with Unprivileged role by default and must verify their email to gain User role.
Account Lifecycle
Login Flow
Users authenticate through Discord OAuth:
1. Login Page
#[get( "/login" )]
pub fn login () -> Template {
Template :: render ( "login" , & {})
}
2. Discord OAuth Initiation
#[get( "/auth/discord" )]
pub fn discord_auth () -> Redirect {
let client_id = std :: env :: var ( "DISCORD_CLIENT_ID" )
. expect ( "DISCORD_CLIENT_ID must be set" );
let redirect_uri = std :: env :: var ( "DISCORD_REDIRECT_URI" )
. expect ( "DISCORD_REDIRECT_URI must be set" );
let discord_auth_url = format! (
"https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify+guilds" ,
client_id , redirect_uri
);
Redirect :: to ( discord_auth_url )
}
3. OAuth Callback
After Discord authentication, the callback handler:
#[get( "/auth/discord/callback?<code>" )]
pub async fn discord_callback (
code : String ,
jar : & CookieJar <' _ >
) -> Result < Template , rocket :: http :: Status > {
let client = reqwest :: Client :: new ();
// Exchange code for access token
let token_response = get_discord_token ( & client , & code ) . await
. map_err ( | _ | rocket :: http :: Status :: InternalServerError ) ? ;
// Get user info
let user_info = get_discord_user ( & client , & token_response . access_token) . await
. map_err ( | _ | rocket :: http :: Status :: InternalServerError ) ? ;
// Verify guild membership
let guilds = get_discord_guilds ( & client , & token_response . access_token) . await
. map_err ( | _ | rocket :: http :: Status :: InternalServerError ) ? ;
let guild_id = std :: env :: var ( "DISCORD_SERVER_ID" )
. expect ( "DISCORD_SERVER_ID must be set" )
. parse :: < u64 >()
. expect ( "DISCORD_SERVER_ID must be a number" );
let is_member = guilds . iter ()
. any ( | guild | guild . id . parse :: < u64 >() . unwrap () == guild_id );
if ! is_member {
return Ok ( Template :: render ( "noAccess" , & {
let mut context = std :: collections :: HashMap :: new ();
context . insert ( "serverName" , "HS Kempten" . to_string ());
context
}));
}
// Create JWT token
let user = User :: new ( user_info . id . parse () . unwrap ());
let token = if user_info . id == "242385294123335690" {
user . create_token ( Roles :: Admin )
} else {
user . create_token ( Roles :: User )
};
// Set cookie
jar . add ( Cookie :: build (( "token" , token ))
. path ( "/" )
. secure ( true )
. http_only ( true ));
Ok ( Template :: render ( "discord_callback" , & {
let mut context = std :: collections :: HashMap :: new ();
context . insert ( "user_id" , user_info . id . clone ());
context . insert ( "username" , user_info . username . clone ());
context . insert ( "avatar" , user_info . get_avatar_url ());
context
}))
}
Users must be members of the configured Discord server (DISCORD_SERVER_ID) to gain access.
Logout
Logging out removes the JWT token cookie:
#[get( "/logout" )]
pub fn logout ( jar : & CookieJar <' _ >) -> Template {
jar . remove ( "token" );
Template :: render ( "logout" , & {})
}
Account Switching
Users can switch between multiple verified accounts:
#[get( "/switch-account" )]
pub fn switch_account ( _user : AuthenticatedUser <' _ >) -> Template {
Template :: render ( "switch-account" , & {})
}
This is useful when:
User has multiple email addresses
Switching between different role levels
Testing with different accounts
Session Management
Checking Login Status
pub async fn is_logged_in ( jar : & CookieJar <' _ >) -> bool {
let key = jar . get ( "token" ) . map ( | cookie | cookie . value ());
match key {
None => false ,
Some ( key ) => User :: verify_token ( key ),
}
}
Token Lifespan
Duration : 24 hours from issuance
Renewal : Users must re-authenticate after expiration
Validation : Checked on every protected route access
Discord user data is fetched during OAuth:
#[derive( Deserialize )]
pub struct UserInfo {
id : String ,
username : String ,
avatar : String ,
}
impl UserInfo {
fn get_avatar_url ( & self ) -> String {
let ext = if self . avatar . starts_with ( "a_" ) { "gif" } else { "png" };
format! (
"https://cdn.discordapp.com/avatars/{}/{}.{}" ,
self . id, self . avatar, ext
)
}
}
This provides:
User ID for identification
Username for display
Avatar URL for profile pictures
Guild Membership Verification
Users must be in the configured Discord server:
#[derive( Deserialize , Debug )]
#[serde(rename_all = "camelCase" )]
pub struct Guild {
id : String ,
name : String ,
icon : Option < String >,
owner : bool ,
permissions : u64 ,
}
pub async fn get_discord_guilds (
client : & reqwest :: Client ,
access_token : & str
) -> Result < Vec < Guild >, reqwest :: Error > {
client . get ( "https://discord.com/api/users/@me/guilds" )
. header ( "Authorization" , format! ( "Bearer {}" , access_token ))
. send ()
. await ?
. json :: < Vec < Guild >>()
. await
}
Non-members are shown the noAccess template.
Environment Configuration
Required environment variables for user management:
Variable Description Example DISCORD_CLIENT_IDDiscord OAuth app ID 123456789012345678DISCORD_CLIENT_SECRETDiscord OAuth secret abc123...DISCORD_REDIRECT_URIOAuth callback URL https://bot.example.com/api/auth/discord/callbackDISCORD_SERVER_IDRequired Discord server ID 987654321098765432SECRET_KEYJWT signing key random_secure_key_here
Keep DISCORD_CLIENT_SECRET and SECRET_KEY private. Never commit them to version control.
Setup Page
First-time setup page for new installations:
#[get( "/setup" )]
pub fn setup () -> Template {
Template :: render ( "setup" , & {})
}
Used for initial configuration and admin account creation.
Best Practices
Token Security
Rotate SECRET_KEY periodically
Use strong, random keys
Set appropriate token expiration
Use HTTPS in production
User Privacy
Store minimal user data
Respect Discord ToS
Clear data on account deletion
Provide data export options
Authentication JWT tokens and role-based access
Discord OAuth API OAuth flow implementation details
Email Verification Student email verification process
Admin Dashboard Administrative user management