Skip to content

[api] Scheduled vote close is broken end-to-end (EventBridge name, at() format, role ARN, panic, and callback auth) #19

Description

@JayDwee

Severity

High — feature cannot work; creating a vote with close_at set 500s after the Discord message was already posted

Related feature ticket: #6

Location

api/src/guilds/votes/controllers.rs:105-155, api/src/middleware.rs:22-33

Problem

Five independent defects in the close_at path:

let target = Target::builder()
    .arn(context.invoked_function_arn)
    .role_arn(context.identity.unwrap().identity_id)   // (1)
    ...
    .name(format!("KB2 Vote Close {}", message.id))     // (2)
    .schedule_expression(format!("at({})", vote_req.close_at.unwrap())) // (3)
  1. Panic: context.identity is only populated for Cognito-identity invocations; it's None behind API Gateway/function URLs → .unwrap() panics. Even when present, a Cognito identity_id is not an IAM role ARN — EventBridge Scheduler needs an execution role ARN.
  2. Invalid name: Scheduler names must match [0-9a-zA-Z\-_.]{1,64}; spaces cause a ValidationException.
  3. Invalid expression: at() requires yyyy-mm-ddThh:mm:ss; DateTime<Utc>'s Display renders 2026-07-02 12:00:00 UTC.
  4. Callback auth cannot succeed: the scheduled payload sends Authorization: Discord <bot_token> (line 108-113). The Discord branch of auth_middleware builds a Bearer {bot_token} client — Discord rejects a bot token with the Bearer prefix, and even if it authenticated, bots are explicitly rejected (if current_user.bot { 401 }).
  5. Even under the Bot scheme, post_votes_id_close extracts Extension<CurrentUser>, which the Bot branch never inserts → 500.

Because the Discord message is created and the vote saved before the schedule call, the client receives a 500 for a vote that was actually created — the UI then can't tell what happened.

Suggested resolution

  • Name/format fixes:
.name(format!("kb2-vote-close-{}", message.id))
.schedule_expression(format!("at({})", close_at.format("%Y-%m-%dT%H:%M:%S")))
  • Pass the scheduler execution role via configuration instead of the Lambda context:
.role_arn(std::env::var("SCHEDULER_ROLE_ARN").expect("SCHEDULER_ROLE_ARN must be set"))
  • Authenticate the callback with the existing Bot scheme (Authorization: Bot <token>), and give the close route a bot-capable path that doesn't require Extension<CurrentUser> — e.g. follow the post_recon pattern (verify/controllers.rs:126-133) of comparing the client token to the bot token, or split an internal close_vote(guild_id, message_id) function that both the admin route and the scheduler target call.
  • Consider creating the schedule before announcing success, or compensating (log + alert) when scheduling fails after the message is posted (see Vote - Scheduled Closing #6's requirement to alert on failed scheduled calls).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions