diff --git a/.devcontainer/api/Dockerfile b/.devcontainer/api/Dockerfile new file mode 100644 index 00000000..0f22ee11 --- /dev/null +++ b/.devcontainer/api/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/devcontainers/go:2-1.25-trixie + +RUN go install github.com/pressly/goose/v3/cmd/goose@latest +RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest +RUN go install go install github.com/hieunguyent12/swag/v2/cmd/swag@bcae90f384b937c1c47dbe9b5d8e7ba8cbe96aac \ No newline at end of file diff --git a/.devcontainer/api/devcontainer.json b/.devcontainer/api/devcontainer.json new file mode 100644 index 00000000..0527063b --- /dev/null +++ b/.devcontainer/api/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "api-devcontainer", + "dockerComposeFile": ["../../docker-compose.yml"], + "service": "api-devcontainer", + "shutdownAction": "none", + "workspaceFolder": "/core/apps/api", + "features": { + "ghcr.io/devcontainers/features/git:1": {} + }, + + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/.devcontainer/web/Dockerfile b/.devcontainer/web/Dockerfile new file mode 100644 index 00000000..4abb7d34 --- /dev/null +++ b/.devcontainer/web/Dockerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:4-24-trixie + +RUN npm install -g pnpm diff --git a/.devcontainer/web/devcontainer.json b/.devcontainer/web/devcontainer.json new file mode 100644 index 00000000..75ae009b --- /dev/null +++ b/.devcontainer/web/devcontainer.json @@ -0,0 +1,26 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "web-devcontainer", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + // "image": "mcr.microsoft.com/devcontainers/typescript-node:4-24-trixie", + "dockerComposeFile": ["../../docker-compose.yml"], + "service": "web-devcontainer", + "shutdownAction": "none", + "workspaceFolder": "/core/apps/web", + "features": { + "ghcr.io/devcontainers/features/git:1": {} + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "pnpm i", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5dc46e6b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf \ No newline at end of file diff --git a/apps/api/Makefile b/apps/api/Makefile index f9b63b38..b33b35bb 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -6,6 +6,14 @@ migrate-up: migrate-down: @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} down +# Use this command when developing inside a dev container. +# Maybe we could figure out a way to connect directly to the host's localhost, instead of using docker network to connect to the postgres db +migrate-up-devcontainer: + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL} up + +migrate-down-devcontainer: + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL} down + generate: @sqlc generate diff --git a/apps/api/docs/docs.go b/apps/api/docs/docs.go index b74cad63..f39bfe29 100644 --- a/apps/api/docs/docs.go +++ b/apps/api/docs/docs.go @@ -144,6 +144,25 @@ const docTemplate = `{ ], "type": "object" }, + "handlers.CreateRedeemableRequest": { + "properties": { + "amount": { + "type": "integer" + }, + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "amount", + "max_user_amount", + "name" + ], + "type": "object" + }, "handlers.CreateTeamRequest": { "properties": { "name": { @@ -193,6 +212,21 @@ const docTemplate = `{ ], "type": "object" }, + "handlers.QueueConfirmationEmailFields": { + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + } + }, + "required": [ + "email", + "firstName" + ], + "type": "object" + }, "handlers.QueueTextEmailRequest": { "properties": { "body": { @@ -261,6 +295,36 @@ const docTemplate = `{ ], "type": "object" }, + "handlers.UpdateRFID": { + "properties": { + "rfid": { + "type": "string" + } + }, + "required": [ + "rfid" + ], + "type": "object" + }, + "handlers.UpdateRedeemableRequest": { + "properties": { + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "total_stock": { + "type": "integer" + } + }, + "required": [ + "max_user_amount", + "name", + "total_stock" + ], + "type": "object" + }, "middleware.UserContext": { "description": "Information about the current user session.", "properties": { @@ -317,6 +381,33 @@ const docTemplate = `{ ], "type": "object" }, + "pgtype.InfinityModifier": { + "enum": [ + 1, + 0, + -1 + ], + "type": "integer", + "x-enum-varnames": [ + "Infinity", + "Finite", + "NegativeInfinity" + ] + }, + "pgtype.Timestamptz": { + "properties": { + "infinityModifier": { + "$ref": "#/components/schemas/pgtype.InfinityModifier" + }, + "time": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + }, + "type": "object" + }, "response.ErrorResponse": { "properties": { "error": { @@ -938,6 +1029,29 @@ const docTemplate = `{ ], "type": "object" }, + "sqlc.GetEventAttendeesWithDiscordRow": { + "properties": { + "discord_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "user_id": { + "type": "string" + } + }, + "required": [ + "discord_id", + "email", + "name", + "user_id" + ], + "type": "object" + }, "sqlc.GetEventStaffRow": { "properties": { "created_at": { @@ -1235,6 +1349,41 @@ const docTemplate = `{ ], "type": "object" }, + "sqlc.Redeemable": { + "properties": { + "amount": { + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/pgtype.Timestamptz" + }, + "event_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "$ref": "#/components/schemas/pgtype.Timestamptz" + } + }, + "required": [ + "amount", + "created_at", + "event_id", + "id", + "max_user_amount", + "name", + "updated_at" + ], + "type": "object" + }, "sqlc.Team": { "properties": { "created_at": { @@ -1501,9 +1650,64 @@ const docTemplate = `{ ] } }, + "/discord/event/{event_id}/attendees": { + "get": { + "description": "Get all attendees for an event who have Discord accounts linked", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "event_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/sqlc.GetEventAttendeesWithDiscordRow" + }, + "type": "array" + } + } + }, + "description": "List of attendees with Discord IDs" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get event attendees with Discord IDs", + "tags": [ + "Discord" + ] + } + }, "/email/queue": { "post": { - "description": "Push an email request to the task queue", + "description": "Push a Confirmation Email request to the task queue", "requestBody": { "content": { "application/json": { @@ -1513,7 +1717,7 @@ const docTemplate = `{ "type": "object" }, { - "$ref": "#/components/schemas/handlers.QueueTextEmailRequest", + "$ref": "#/components/schemas/handlers.QueueConfirmationEmailFields", "summary": "request", "description": "Email data" } @@ -1556,7 +1760,7 @@ const docTemplate = `{ "description": "Server Error: The server went kaput while queueing email sending" } }, - "summary": "Queue an Email Request", + "summary": "Queue a Confirmation Email Request", "tags": [ "Email" ] @@ -1893,7 +2097,7 @@ const docTemplate = `{ }, "/events/{eventId}/application/accept-acceptance": { "patch": { - "description": "Sets application status from accepted to rejected", + "description": "Sets event role to attendee, from applicant", "parameters": [ { "description": "ID of the event to join the waitlist for", @@ -1907,7 +2111,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Acceptance withdrawn joined successfully" + "description": "Acceptance successful" }, "400": { "content": { @@ -2406,12 +2610,57 @@ const docTemplate = `{ ] } }, + "/events/{eventId}/application/transition-waitlisted-applications": { + "patch": { + "description": "Transitions all accepted users to waitlist, and accepts 50 from the waitlist.", + "parameters": [ + { + "description": "ID of the event to join the waitlist for", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Transitioned application statuses successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to transition application statuses" + } + }, + "summary": "Sets application status from accepted to rejected", + "tags": [ + "Application Event" + ] + } + }, "/events/{eventId}/application/withdraw-acceptance": { "patch": { "description": "Sets application status from accepted to rejected", "parameters": [ { - "description": "ID of the event to join the waitlist for", + "description": "ID of the event to withdraw acceptance from", "in": "path", "name": "eventId", "required": true, @@ -2422,7 +2671,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Acceptance withdrawn joined successfully" + "description": "Acceptance withdrawn successfully" }, "400": { "content": { @@ -2442,7 +2691,7 @@ const docTemplate = `{ } } }, - "description": "Server error: failed to withdraw" + "description": "Server error: failed to withdraw acceptance" } }, "summary": "Withdraw an acceptance after being accepted to an event.", @@ -2451,6 +2700,51 @@ const docTemplate = `{ ] } }, + "/events/{eventId}/application/withdraw-attendance": { + "patch": { + "description": "Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.", + "parameters": [ + { + "description": "ID of the event to withdraw attendance from", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Attendance withdrawn successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to withdraw attendance" + } + }, + "summary": "Withdraw attendance after accepting to go to an event.", + "tags": [ + "Application" + ] + } + }, "/events/{eventId}/application/{applicationId}": { "get": { "description": "Retrieves an application using the user id and event id primary keys and unique constraints. Only accessible by event staff and admins.", @@ -2640,7 +2934,7 @@ const docTemplate = `{ }, "/events/{eventId}/bat-runs": { "delete": { - "description": "Delete an existing event", + "description": "Delete an existing BAT run", "parameters": [ { "description": "Run ID", @@ -2677,7 +2971,7 @@ const docTemplate = `{ "description": "Server Error: Something went terribly wrong on our end." } }, - "summary": "Delete an event", + "summary": "Delete a run", "tags": [ "Bat" ] @@ -2791,24 +3085,96 @@ const docTemplate = `{ ] } }, - "/events/{eventId}/interest": { - "post": { - "description": "Submit email for event interest/mailing list", + "/events/{eventId}/discord/{discordId}": { + "get": { + "description": "Get the event role for a user based on their Discord account ID and a specific event ID", "parameters": [ { - "description": "Event ID", + "description": "Event ID (UUID)", "in": "path", "name": "eventId", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { + }, + { + "description": "Discord account ID", + "in": "path", + "name": "discordId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": {}, + "type": "object" + } + } + }, + "description": "role" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid event ID or discord ID" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User or role not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get user event role by Discord ID and Event ID", + "tags": [ + "Discord" + ] + } + }, + "/events/{eventId}/interest": { + "post": { + "description": "Submit email for event interest/mailing list", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { "oneOf": [ { "type": "object" @@ -2914,6 +3280,164 @@ const docTemplate = `{ ] } }, + "/events/{eventId}/queue-transition-waitlist-task": { + "post": { + "description": "Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.", + "responses": { + "200": { + "description": "Scheduler shutdown successfully" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to shutdown scheduler" + } + }, + "summary": "Shutsdown an asynq scheduler", + "tags": [ + "" + ] + } + }, + "/events/{eventId}/redeemables": { + "get": { + "description": "Retrieve a list of all redeemable items associated with a specific event ID.", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/sqlc.Redeemable" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Missing or invalid Event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get all redeemables for an event", + "tags": [ + "Redeemables" + ] + }, + "post": { + "description": "Create a new redeemable item for a specific event.", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.CreateRedeemableRequest", + "summary": "request", + "description": "Redeemable creation data" + } + ] + } + } + }, + "description": "Redeemable creation data", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sqlc.Redeemable" + } + } + }, + "description": "Created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid request body or ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Create a new redeemable", + "tags": [ + "Redeemables" + ] + } + }, "/events/{eventId}/review-status": { "get": { "description": "Check if application reviews complete", @@ -3247,6 +3771,50 @@ const docTemplate = `{ ] } }, + "/events/{eventId}/send-welcome-emails": { + "post": { + "parameters": [ + { + "description": "ID of the event", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Welcome emails began to queue successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to begin queuing welcome emails" + } + }, + "summary": "Sends welcome emails to attendees", + "tags": [ + "" + ] + } + }, "/events/{eventId}/staff": { "get": { "description": "Gets all users with role STAFF or ADMIN", @@ -3771,20 +4339,95 @@ const docTemplate = `{ ] } }, - "/events/{eventId}/users/{userId}": { + "/events/{eventId}/users/by-rfid/{rfid}": { "get": { - "description": "A user's information along with their event details such as check in state, role, and more. Must be validated on the frontend.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.UserInfoForEvent" - } - } - }, - "description": "OK" - }, + "description": "Looks up a user's ID by their RFID code for a specific event. Returns the user ID which can be used for other operations.", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "RFID code (10 digits)", + "in": "path", + "name": "rfid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + } + }, + "description": "OK - Returns user ID" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request/Malformed request." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User not found with the provided RFID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server Error: error getting user by RFID" + } + }, + "summary": "Retrieves a user's ID by their RFID", + "tags": [ + "Event" + ] + } + }, + "/events/{eventId}/users/{userId}": { + "get": { + "description": "A user's information along with their event details such as check in state, role, and more. Must be validated on the frontend.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/services.UserInfoForEvent" + } + } + }, + "description": "OK" + }, "400": { "content": { "application/json": { @@ -3812,6 +4455,261 @@ const docTemplate = `{ ] } }, + "/events/{eventId}/users/{userId}/update-rfid": { + "post": { + "description": "Associates a new RFID string with a specific user for the given event. This overwrites any existing RFID association.", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.UpdateRFID", + "summary": "body", + "description": "New RFID data" + } + ] + } + } + }, + "description": "New RFID data", + "required": true + }, + "responses": { + "204": { + "description": "No Content - RFID updated successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid request body or UUID format" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User or Event not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Updates a user's RFID tag", + "tags": [ + "Event" + ] + } + }, + "/redeemables/{redeemableId}": { + "delete": { + "description": "Permanently delete a redeemable item by ID.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid Redeemable ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Delete a redeemable", + "tags": [ + "Redeemables" + ] + }, + "patch": { + "description": "Update specific fields (name, stock, max per user) of a redeemable.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.UpdateRedeemableRequest", + "summary": "request", + "description": "Redeemable update data (partial fields allowed)" + } + ] + } + } + }, + "description": "Redeemable update data (partial fields allowed)", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sqlc.Redeemable" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid ID or request body" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Update an existing redeemable", + "tags": [ + "Redeemables" + ] + } + }, + "/redeemables/{redeemableId}/users/{userId}": { + "post": { + "description": "Create a redemption record linking a specific user to a redeemable item.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User ID (UUID)", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid IDs" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Redeem an item for a user", + "tags": [ + "Redeemables" + ] + } + }, "/teams/join/{requestId}/accept": { "post": { "description": "Accepts a pending team join request. Only the team owner can perform this action.", diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json index ee05b681..4a5d408a 100644 --- a/apps/api/docs/swagger.json +++ b/apps/api/docs/swagger.json @@ -137,6 +137,25 @@ ], "type": "object" }, + "handlers.CreateRedeemableRequest": { + "properties": { + "amount": { + "type": "integer" + }, + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "amount", + "max_user_amount", + "name" + ], + "type": "object" + }, "handlers.CreateTeamRequest": { "properties": { "name": { @@ -186,6 +205,21 @@ ], "type": "object" }, + "handlers.QueueConfirmationEmailFields": { + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + } + }, + "required": [ + "email", + "firstName" + ], + "type": "object" + }, "handlers.QueueTextEmailRequest": { "properties": { "body": { @@ -254,6 +288,36 @@ ], "type": "object" }, + "handlers.UpdateRFID": { + "properties": { + "rfid": { + "type": "string" + } + }, + "required": [ + "rfid" + ], + "type": "object" + }, + "handlers.UpdateRedeemableRequest": { + "properties": { + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "total_stock": { + "type": "integer" + } + }, + "required": [ + "max_user_amount", + "name", + "total_stock" + ], + "type": "object" + }, "middleware.UserContext": { "description": "Information about the current user session.", "properties": { @@ -310,6 +374,33 @@ ], "type": "object" }, + "pgtype.InfinityModifier": { + "enum": [ + 1, + 0, + -1 + ], + "type": "integer", + "x-enum-varnames": [ + "Infinity", + "Finite", + "NegativeInfinity" + ] + }, + "pgtype.Timestamptz": { + "properties": { + "infinityModifier": { + "$ref": "#/components/schemas/pgtype.InfinityModifier" + }, + "time": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + }, + "type": "object" + }, "response.ErrorResponse": { "properties": { "error": { @@ -931,6 +1022,29 @@ ], "type": "object" }, + "sqlc.GetEventAttendeesWithDiscordRow": { + "properties": { + "discord_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "user_id": { + "type": "string" + } + }, + "required": [ + "discord_id", + "email", + "name", + "user_id" + ], + "type": "object" + }, "sqlc.GetEventStaffRow": { "properties": { "created_at": { @@ -1228,6 +1342,41 @@ ], "type": "object" }, + "sqlc.Redeemable": { + "properties": { + "amount": { + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/pgtype.Timestamptz" + }, + "event_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "max_user_amount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "$ref": "#/components/schemas/pgtype.Timestamptz" + } + }, + "required": [ + "amount", + "created_at", + "event_id", + "id", + "max_user_amount", + "name", + "updated_at" + ], + "type": "object" + }, "sqlc.Team": { "properties": { "created_at": { @@ -1494,9 +1643,64 @@ ] } }, + "/discord/event/{event_id}/attendees": { + "get": { + "description": "Get all attendees for an event who have Discord accounts linked", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "event_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/sqlc.GetEventAttendeesWithDiscordRow" + }, + "type": "array" + } + } + }, + "description": "List of attendees with Discord IDs" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get event attendees with Discord IDs", + "tags": [ + "Discord" + ] + } + }, "/email/queue": { "post": { - "description": "Push an email request to the task queue", + "description": "Push a Confirmation Email request to the task queue", "requestBody": { "content": { "application/json": { @@ -1506,7 +1710,7 @@ "type": "object" }, { - "$ref": "#/components/schemas/handlers.QueueTextEmailRequest", + "$ref": "#/components/schemas/handlers.QueueConfirmationEmailFields", "summary": "request", "description": "Email data" } @@ -1549,7 +1753,7 @@ "description": "Server Error: The server went kaput while queueing email sending" } }, - "summary": "Queue an Email Request", + "summary": "Queue a Confirmation Email Request", "tags": [ "Email" ] @@ -1886,7 +2090,7 @@ }, "/events/{eventId}/application/accept-acceptance": { "patch": { - "description": "Sets application status from accepted to rejected", + "description": "Sets event role to attendee, from applicant", "parameters": [ { "description": "ID of the event to join the waitlist for", @@ -1900,7 +2104,7 @@ ], "responses": { "200": { - "description": "Acceptance withdrawn joined successfully" + "description": "Acceptance successful" }, "400": { "content": { @@ -2399,12 +2603,57 @@ ] } }, + "/events/{eventId}/application/transition-waitlisted-applications": { + "patch": { + "description": "Transitions all accepted users to waitlist, and accepts 50 from the waitlist.", + "parameters": [ + { + "description": "ID of the event to join the waitlist for", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Transitioned application statuses successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to transition application statuses" + } + }, + "summary": "Sets application status from accepted to rejected", + "tags": [ + "Application Event" + ] + } + }, "/events/{eventId}/application/withdraw-acceptance": { "patch": { "description": "Sets application status from accepted to rejected", "parameters": [ { - "description": "ID of the event to join the waitlist for", + "description": "ID of the event to withdraw acceptance from", "in": "path", "name": "eventId", "required": true, @@ -2415,7 +2664,7 @@ ], "responses": { "200": { - "description": "Acceptance withdrawn joined successfully" + "description": "Acceptance withdrawn successfully" }, "400": { "content": { @@ -2435,7 +2684,7 @@ } } }, - "description": "Server error: failed to withdraw" + "description": "Server error: failed to withdraw acceptance" } }, "summary": "Withdraw an acceptance after being accepted to an event.", @@ -2444,6 +2693,51 @@ ] } }, + "/events/{eventId}/application/withdraw-attendance": { + "patch": { + "description": "Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.", + "parameters": [ + { + "description": "ID of the event to withdraw attendance from", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Attendance withdrawn successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to withdraw attendance" + } + }, + "summary": "Withdraw attendance after accepting to go to an event.", + "tags": [ + "Application" + ] + } + }, "/events/{eventId}/application/{applicationId}": { "get": { "description": "Retrieves an application using the user id and event id primary keys and unique constraints. Only accessible by event staff and admins.", @@ -2633,7 +2927,7 @@ }, "/events/{eventId}/bat-runs": { "delete": { - "description": "Delete an existing event", + "description": "Delete an existing BAT run", "parameters": [ { "description": "Run ID", @@ -2670,7 +2964,7 @@ "description": "Server Error: Something went terribly wrong on our end." } }, - "summary": "Delete an event", + "summary": "Delete a run", "tags": [ "Bat" ] @@ -2784,24 +3078,96 @@ ] } }, - "/events/{eventId}/interest": { - "post": { - "description": "Submit email for event interest/mailing list", + "/events/{eventId}/discord/{discordId}": { + "get": { + "description": "Get the event role for a user based on their Discord account ID and a specific event ID", "parameters": [ { - "description": "Event ID", + "description": "Event ID (UUID)", "in": "path", "name": "eventId", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { + }, + { + "description": "Discord account ID", + "in": "path", + "name": "discordId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": {}, + "type": "object" + } + } + }, + "description": "role" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid event ID or discord ID" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User or role not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get user event role by Discord ID and Event ID", + "tags": [ + "Discord" + ] + } + }, + "/events/{eventId}/interest": { + "post": { + "description": "Submit email for event interest/mailing list", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { "oneOf": [ { "type": "object" @@ -2907,6 +3273,164 @@ ] } }, + "/events/{eventId}/queue-transition-waitlist-task": { + "post": { + "description": "Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.", + "responses": { + "200": { + "description": "Scheduler shutdown successfully" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to shutdown scheduler" + } + }, + "summary": "Shutsdown an asynq scheduler", + "tags": [ + "" + ] + } + }, + "/events/{eventId}/redeemables": { + "get": { + "description": "Retrieve a list of all redeemable items associated with a specific event ID.", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/sqlc.Redeemable" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Missing or invalid Event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get all redeemables for an event", + "tags": [ + "Redeemables" + ] + }, + "post": { + "description": "Create a new redeemable item for a specific event.", + "parameters": [ + { + "description": "Event ID (UUID)", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.CreateRedeemableRequest", + "summary": "request", + "description": "Redeemable creation data" + } + ] + } + } + }, + "description": "Redeemable creation data", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sqlc.Redeemable" + } + } + }, + "description": "Created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid request body or ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Create a new redeemable", + "tags": [ + "Redeemables" + ] + } + }, "/events/{eventId}/review-status": { "get": { "description": "Check if application reviews complete", @@ -3240,6 +3764,50 @@ ] } }, + "/events/{eventId}/send-welcome-emails": { + "post": { + "parameters": [ + { + "description": "ID of the event", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Welcome emails began to queue successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request: invalid event ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server error: failed to begin queuing welcome emails" + } + }, + "summary": "Sends welcome emails to attendees", + "tags": [ + "" + ] + } + }, "/events/{eventId}/staff": { "get": { "description": "Gets all users with role STAFF or ADMIN", @@ -3764,20 +4332,95 @@ ] } }, - "/events/{eventId}/users/{userId}": { + "/events/{eventId}/users/by-rfid/{rfid}": { "get": { - "description": "A user's information along with their event details such as check in state, role, and more. Must be validated on the frontend.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.UserInfoForEvent" - } - } - }, - "description": "OK" - }, + "description": "Looks up a user's ID by their RFID code for a specific event. Returns the user ID which can be used for other operations.", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "RFID code (10 digits)", + "in": "path", + "name": "rfid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + } + }, + "description": "OK - Returns user ID" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Bad request/Malformed request." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User not found with the provided RFID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Server Error: error getting user by RFID" + } + }, + "summary": "Retrieves a user's ID by their RFID", + "tags": [ + "Event" + ] + } + }, + "/events/{eventId}/users/{userId}": { + "get": { + "description": "A user's information along with their event details such as check in state, role, and more. Must be validated on the frontend.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/services.UserInfoForEvent" + } + } + }, + "description": "OK" + }, "400": { "content": { "application/json": { @@ -3805,6 +4448,261 @@ ] } }, + "/events/{eventId}/users/{userId}/update-rfid": { + "post": { + "description": "Associates a new RFID string with a specific user for the given event. This overwrites any existing RFID association.", + "parameters": [ + { + "description": "Event ID", + "in": "path", + "name": "eventId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "User ID", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.UpdateRFID", + "summary": "body", + "description": "New RFID data" + } + ] + } + } + }, + "description": "New RFID data", + "required": true + }, + "responses": { + "204": { + "description": "No Content - RFID updated successfully" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid request body or UUID format" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "User or Event not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Updates a user's RFID tag", + "tags": [ + "Event" + ] + } + }, + "/redeemables/{redeemableId}": { + "delete": { + "description": "Permanently delete a redeemable item by ID.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid Redeemable ID" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Delete a redeemable", + "tags": [ + "Redeemables" + ] + }, + "patch": { + "description": "Update specific fields (name, stock, max per user) of a redeemable.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "$ref": "#/components/schemas/handlers.UpdateRedeemableRequest", + "summary": "request", + "description": "Redeemable update data (partial fields allowed)" + } + ] + } + } + }, + "description": "Redeemable update data (partial fields allowed)", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sqlc.Redeemable" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid ID or request body" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Update an existing redeemable", + "tags": [ + "Redeemables" + ] + } + }, + "/redeemables/{redeemableId}/users/{userId}": { + "post": { + "description": "Create a redemption record linking a specific user to a redeemable item.", + "parameters": [ + { + "description": "Redeemable ID (UUID)", + "in": "path", + "name": "redeemableId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User ID (UUID)", + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Invalid IDs" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Redeem an item for a user", + "tags": [ + "Redeemables" + ] + } + }, "/teams/join/{requestId}/accept": { "post": { "description": "Accepts a pending team join request. Only the team owner can perform this action.", diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml index dfb7bc38..bcfea44f 100644 --- a/apps/api/docs/swagger.yaml +++ b/apps/api/docs/swagger.yaml @@ -95,6 +95,19 @@ components: required: - message type: object + handlers.CreateRedeemableRequest: + properties: + amount: + type: integer + max_user_amount: + type: integer + name: + type: string + required: + - amount + - max_user_amount + - name + type: object handlers.CreateTeamRequest: properties: name: @@ -128,6 +141,16 @@ components: - role - user_id type: object + handlers.QueueConfirmationEmailFields: + properties: + email: + type: string + firstName: + type: string + required: + - email + - firstName + type: object handlers.QueueTextEmailRequest: properties: body: @@ -175,6 +198,26 @@ components: - name - preferred_email type: object + handlers.UpdateRFID: + properties: + rfid: + type: string + required: + - rfid + type: object + handlers.UpdateRedeemableRequest: + properties: + max_user_amount: + type: integer + name: + type: string + total_stock: + type: integer + required: + - max_user_amount + - name + - total_stock + type: object middleware.UserContext: description: Information about the current user session. properties: @@ -220,6 +263,25 @@ components: - role - userId type: object + pgtype.InfinityModifier: + enum: + - 1 + - 0 + - -1 + type: integer + x-enum-varnames: + - Infinity + - Finite + - NegativeInfinity + pgtype.Timestamptz: + properties: + infinityModifier: + $ref: '#/components/schemas/pgtype.InfinityModifier' + time: + type: string + valid: + type: boolean + type: object response.ErrorResponse: properties: error: @@ -667,6 +729,22 @@ components: - waitlisted - withdrawn type: object + sqlc.GetEventAttendeesWithDiscordRow: + properties: + discord_id: + type: string + email: + type: string + name: + type: string + user_id: + type: string + required: + - discord_id + - email + - name + - user_id + type: object sqlc.GetEventStaffRow: properties: created_at: @@ -882,6 +960,31 @@ components: - event_role_type - valid type: object + sqlc.Redeemable: + properties: + amount: + type: integer + created_at: + $ref: '#/components/schemas/pgtype.Timestamptz' + event_id: + type: string + id: + type: string + max_user_amount: + type: integer + name: + type: string + updated_at: + $ref: '#/components/schemas/pgtype.Timestamptz' + required: + - amount + - created_at + - event_id + - id + - max_user_amount + - name + - updated_at + type: object sqlc.Team: properties: created_at: @@ -1063,16 +1166,50 @@ paths: summary: Get Current User​ tags: - Authentication + /discord/event/{event_id}/attendees: + get: + description: Get all attendees for an event who have Discord accounts linked + parameters: + - description: Event ID (UUID) + in: path + name: event_id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/sqlc.GetEventAttendeesWithDiscordRow' + type: array + description: List of attendees with Discord IDs + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid event ID + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal server error + summary: Get event attendees with Discord IDs + tags: + - Discord /email/queue: post: - description: Push an email request to the task queue + description: Push a Confirmation Email request to the task queue requestBody: content: application/json: schema: oneOf: - type: object - - $ref: '#/components/schemas/handlers.QueueTextEmailRequest' + - $ref: '#/components/schemas/handlers.QueueConfirmationEmailFields' description: Email data summary: request description: Email data @@ -1097,7 +1234,7 @@ paths: schema: $ref: '#/components/schemas/response.ErrorResponse' description: 'Server Error: The server went kaput while queueing email sending' - summary: Queue an Email Request + summary: Queue a Confirmation Email Request tags: - Email /events: @@ -1427,7 +1564,7 @@ paths: - Application /events/{eventId}/application/accept-acceptance: patch: - description: Sets application status from accepted to rejected + description: Sets event role to attendee, from applicant parameters: - description: ID of the event to join the waitlist for in: path @@ -1437,7 +1574,7 @@ paths: type: string responses: "200": - description: Acceptance withdrawn joined successfully + description: Acceptance successful "400": content: application/json: @@ -1750,11 +1887,40 @@ paths: summary: Submit Application tags: - Application + /events/{eventId}/application/transition-waitlisted-applications: + patch: + description: Transitions all accepted users to waitlist, and accepts 50 from + the waitlist. + parameters: + - description: ID of the event to join the waitlist for + in: path + name: eventId + required: true + schema: + type: string + responses: + "200": + description: Transitioned application statuses successfully + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Bad request: invalid event ID' + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Server error: failed to transition application statuses' + summary: Sets application status from accepted to rejected + tags: + - Application Event /events/{eventId}/application/withdraw-acceptance: patch: description: Sets application status from accepted to rejected parameters: - - description: ID of the event to join the waitlist for + - description: ID of the event to withdraw acceptance from in: path name: eventId required: true @@ -1762,7 +1928,7 @@ paths: type: string responses: "200": - description: Acceptance withdrawn joined successfully + description: Acceptance withdrawn successfully "400": content: application/json: @@ -1774,13 +1940,42 @@ paths: application/json: schema: $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server error: failed to withdraw' + description: 'Server error: failed to withdraw acceptance' summary: Withdraw an acceptance after being accepted to an event. tags: - Application + /events/{eventId}/application/withdraw-attendance: + patch: + description: Sets application status from accepted to withdrawn. Sets event + role from attendee, back to applicant. + parameters: + - description: ID of the event to withdraw attendance from + in: path + name: eventId + required: true + schema: + type: string + responses: + "200": + description: Attendance withdrawn successfully + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Bad request: invalid event ID' + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Server error: failed to withdraw attendance' + summary: Withdraw attendance after accepting to go to an event. + tags: + - Application /events/{eventId}/bat-runs: delete: - description: Delete an existing event + description: Delete an existing BAT run parameters: - description: Run ID in: path @@ -1803,7 +1998,7 @@ paths: schema: $ref: '#/components/schemas/response.ErrorResponse' description: 'Server Error: Something went terribly wrong on our end.' - summary: Delete an event + summary: Delete a run tags: - Bat get: @@ -1875,6 +2070,52 @@ paths: summary: Check a user into an event tags: - Admissions + /events/{eventId}/discord/{discordId}: + get: + description: Get the event role for a user based on their Discord account ID + and a specific event ID + parameters: + - description: Event ID (UUID) + in: path + name: eventId + required: true + schema: + type: string + - description: Discord account ID + in: path + name: discordId + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + additionalProperties: {} + type: object + description: role + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid event ID or discord ID + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: User or role not found + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal server error + summary: Get user event role by Discord ID and Event ID + tags: + - Discord /events/{eventId}/interest: post: description: Submit email for event interest/mailing list @@ -1950,6 +2191,104 @@ paths: summary: Retrieves general information about the event tags: - Event + /events/{eventId}/queue-transition-waitlist-task: + post: + description: Shutsdown the scheduler used for the waitlist transition task. + Error returned through logs if a scheduler is not active. + responses: + "200": + description: Scheduler shutdown successfully + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Server error: failed to shutdown scheduler' + summary: Shutsdown an asynq scheduler + tags: + - "" + /events/{eventId}/redeemables: + get: + description: Retrieve a list of all redeemable items associated with a specific + event ID. + parameters: + - description: Event ID (UUID) + in: path + name: eventId + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/sqlc.Redeemable' + type: array + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Missing or invalid Event ID + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal Server Error + summary: Get all redeemables for an event + tags: + - Redeemables + post: + description: Create a new redeemable item for a specific event. + parameters: + - description: Event ID (UUID) + in: path + name: eventId + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/handlers.CreateRedeemableRequest' + description: Redeemable creation data + summary: request + description: Redeemable creation data + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/sqlc.Redeemable' + description: Created + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid request body or ID + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal Server Error + summary: Create a new redeemable + tags: + - Redeemables /events/{eventId}/review-status: get: description: Check if application reviews complete @@ -2154,6 +2493,33 @@ paths: summary: Change or add event role of a user in batch tags: - Event + /events/{eventId}/send-welcome-emails: + post: + parameters: + - description: ID of the event + in: path + name: eventId + required: true + schema: + type: string + responses: + "200": + description: Welcome emails began to queue successfully + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Bad request: invalid event ID' + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Server error: failed to begin queuing welcome emails' + summary: Sends welcome emails to attendees + tags: + - "" /events/{eventId}/staff: get: description: Gets all users with role STAFF or ADMIN @@ -2509,6 +2875,213 @@ paths: summary: Retrieves a user's information along with their event information tags: - Event + /events/{eventId}/users/{userId}/update-rfid: + post: + description: Associates a new RFID string with a specific user for the given + event. This overwrites any existing RFID association. + parameters: + - description: Event ID + in: path + name: eventId + required: true + schema: + format: uuid + type: string + - description: User ID + in: path + name: userId + required: true + schema: + format: uuid + type: string + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/handlers.UpdateRFID' + description: New RFID data + summary: body + description: New RFID data + required: true + responses: + "204": + description: No Content - RFID updated successfully + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid request body or UUID format + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: User or Event not found + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal server error + summary: Updates a user's RFID tag + tags: + - Event + /events/{eventId}/users/by-rfid/{rfid}: + get: + description: Looks up a user's ID by their RFID code for a specific event. Returns + the user ID which can be used for other operations. + parameters: + - description: Event ID + in: path + name: eventId + required: true + schema: + format: uuid + type: string + - description: RFID code (10 digits) + in: path + name: rfid + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: OK - Returns user ID + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Bad request/Malformed request. + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: User not found with the provided RFID + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: 'Server Error: error getting user by RFID' + summary: Retrieves a user's ID by their RFID + tags: + - Event + /redeemables/{redeemableId}: + delete: + description: Permanently delete a redeemable item by ID. + parameters: + - description: Redeemable ID (UUID) + in: path + name: redeemableId + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid Redeemable ID + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal Server Error + summary: Delete a redeemable + tags: + - Redeemables + patch: + description: Update specific fields (name, stock, max per user) of a redeemable. + parameters: + - description: Redeemable ID (UUID) + in: path + name: redeemableId + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/handlers.UpdateRedeemableRequest' + description: Redeemable update data (partial fields allowed) + summary: request + description: Redeemable update data (partial fields allowed) + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/sqlc.Redeemable' + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid ID or request body + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal Server Error + summary: Update an existing redeemable + tags: + - Redeemables + /redeemables/{redeemableId}/users/{userId}: + post: + description: Create a redemption record linking a specific user to a redeemable + item. + parameters: + - description: Redeemable ID (UUID) + in: path + name: redeemableId + required: true + schema: + type: string + - description: User ID (UUID) + in: path + name: userId + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Invalid IDs + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/response.ErrorResponse' + description: Internal Server Error + summary: Redeem an item for a user + tags: + - Redeemables /teams/{teamId}: get: description: Retrieves the team information and the full list of team members diff --git a/docker-compose.yml b/docker-compose.yml index 3e4e7460..5dedce8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,6 +87,22 @@ services: retries: 5 start_period: 5s + web-devcontainer: + build: + context: ./.devcontainer/web + dockerfile: Dockerfile + volumes: + - .:/core + - /core/apps/web/node_modules + command: sleep infinity + + api-devcontainer: + build: + context: ./.devcontainer/api + dockerfile: Dockerfile + volumes: + - .:/core + command: sleep infinity volumes: postgres_data: