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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions