Skip to main content
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:
src/web/auth.rs
#[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:
src/web/auth.rs
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

src/web/mod.rs
#[get("/login")]
pub fn login() -> Template {
    Template::render("login", &{})
}

2. Discord OAuth Initiation

src/web/api/mod.rs
#[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:
src/web/api/mod.rs
#[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:
src/web/mod.rs
#[get("/logout")]
pub fn logout(jar: &CookieJar<'_>) -> Template {
    jar.remove("token");
    Template::render("logout", &{})
}

Account Switching

Users can switch between multiple verified accounts:
src/web/mod.rs
#[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

src/web/auth.rs
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 Information

Discord user data is fetched during OAuth:
src/web/api/mod.rs
#[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:
src/web/api/mod.rs
#[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:
VariableDescriptionExample
DISCORD_CLIENT_IDDiscord OAuth app ID123456789012345678
DISCORD_CLIENT_SECRETDiscord OAuth secretabc123...
DISCORD_REDIRECT_URIOAuth callback URLhttps://bot.example.com/api/auth/discord/callback
DISCORD_SERVER_IDRequired Discord server ID987654321098765432
SECRET_KEYJWT signing keyrandom_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:
src/web/mod.rs
#[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

Build docs developers (and LLMs) love