From b8eddf941fdb596e46bed4cc5e1c0743b0dce151 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Thu, 12 Mar 2026 23:16:48 -0400 Subject: [PATCH 01/11] feat: added configs for setting up devcontainers --- .devcontainer/api/Dockerfile | 1 + .devcontainer/api/devcontainer.json | 25 +++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 17 +++++++++++++++++ .devcontainer/web/Dockerfile | 3 +++ .devcontainer/web/devcontainer.json | 26 ++++++++++++++++++++++++++ .gitattributes | 3 +++ 6 files changed, 75 insertions(+) create mode 100644 .devcontainer/api/Dockerfile create mode 100644 .devcontainer/api/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/web/Dockerfile create mode 100644 .devcontainer/web/devcontainer.json create mode 100644 .gitattributes diff --git a/.devcontainer/api/Dockerfile b/.devcontainer/api/Dockerfile new file mode 100644 index 00000000..19a36217 --- /dev/null +++ b/.devcontainer/api/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/devcontainers/go:2-1.25-trixie \ No newline at end of file diff --git a/.devcontainer/api/devcontainer.json b/.devcontainer/api/devcontainer.json new file mode 100644 index 00000000..d3caa09b --- /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/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..26e9b6fb --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,17 @@ +services: + web-devcontainer: + build: + context: ./web + dockerfile: Dockerfile + volumes: + - ..:/core + - /core/apps/web/node_modules + command: sleep infinity + + api-devcontainer: + build: + context: ./api + dockerfile: Dockerfile + volumes: + - ..:/core + command: sleep infinity \ No newline at end of file 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..bb50c478 --- /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 From 4088aac1c2e91280ccce90ea2183bee54afc77c8 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 18 Mar 2026 19:37:59 -0400 Subject: [PATCH 02/11] chore: minor changes to configs --- .devcontainer/api/Dockerfile | 5 ++++- .devcontainer/api/devcontainer.json | 6 +++--- .devcontainer/docker-compose.yml | 17 ----------------- .devcontainer/web/devcontainer.json | 2 +- apps/api/Makefile | 8 ++++++++ docker-compose.yml | 16 ++++++++++++++++ 6 files changed, 32 insertions(+), 22 deletions(-) delete mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/api/Dockerfile b/.devcontainer/api/Dockerfile index 19a36217..78ef94be 100644 --- a/.devcontainer/api/Dockerfile +++ b/.devcontainer/api/Dockerfile @@ -1 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/go:2-1.25-trixie \ No newline at end of file +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 \ No newline at end of file diff --git a/.devcontainer/api/devcontainer.json b/.devcontainer/api/devcontainer.json index d3caa09b..0527063b 100644 --- a/.devcontainer/api/devcontainer.json +++ b/.devcontainer/api/devcontainer.json @@ -2,13 +2,13 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { "name": "api-devcontainer", - "dockerComposeFile": ["../docker-compose.yml"], + "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. @@ -21,5 +21,5 @@ // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "remoteUser": "root" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 26e9b6fb..00000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - web-devcontainer: - build: - context: ./web - dockerfile: Dockerfile - volumes: - - ..:/core - - /core/apps/web/node_modules - command: sleep infinity - - api-devcontainer: - build: - context: ./api - dockerfile: Dockerfile - volumes: - - ..:/core - command: sleep infinity \ No newline at end of file diff --git a/.devcontainer/web/devcontainer.json b/.devcontainer/web/devcontainer.json index bb50c478..75ae009b 100644 --- a/.devcontainer/web/devcontainer.json +++ b/.devcontainer/web/devcontainer.json @@ -4,7 +4,7 @@ "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"], + "dockerComposeFile": ["../../docker-compose.yml"], "service": "web-devcontainer", "shutdownAction": "none", "workspaceFolder": "/core/apps/web", 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/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: From e3da9cdd21b724f6c70b22e1b970d587007f6e23 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 18 Mar 2026 20:00:29 -0400 Subject: [PATCH 03/11] chore: install swag (hieu's version) --- .devcontainer/api/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/api/Dockerfile b/.devcontainer/api/Dockerfile index 78ef94be..0f22ee11 100644 --- a/.devcontainer/api/Dockerfile +++ b/.devcontainer/api/Dockerfile @@ -1,4 +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 \ No newline at end of file +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 From b183563b7f1b08795c51bb36fa961c019dfe9d3e Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Wed, 18 Mar 2026 20:00:47 -0400 Subject: [PATCH 04/11] chore: update docs --- apps/api/docs/docs.go | 964 +++++++++++++++++++++++++++++++++++-- apps/api/docs/swagger.json | 964 +++++++++++++++++++++++++++++++++++-- apps/api/docs/swagger.yaml | 593 ++++++++++++++++++++++- 3 files changed, 2445 insertions(+), 76 deletions(-) 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 From 1c1ca36fc30494cabafb52513fee0e22523ba0f1 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Mon, 23 Mar 2026 08:53:14 -0400 Subject: [PATCH 05/11] chore: disable devcontainers in docker-compose for now --- docker-compose.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5dedce8e..e0a3a6b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,22 +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 + # 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 + # api-devcontainer: + # build: + # context: ./.devcontainer/api + # dockerfile: Dockerfile + # volumes: + # - .:/core + # command: sleep infinity volumes: postgres_data: From 6847bffb4f85cd47646e648500d2d82fe5b72f59 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen <76720778+hieunguyent12@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:03:15 -0400 Subject: [PATCH 06/11] Breaking change: New API (#322) * refactor: api rewrite initial commit * refactor: added hackathon and user domains, and other improvements * refactor: added roles middleware * refactor: added more handlers + services for user domain * refactor: added application and email domains and other changes * refactor: team domain and other changes * refactor: redeemables * refactor: checkin user * refactor: bat and email service and more changes * a lot of changes * fix auth middlewares and changed sqlc types * small changes to db schemas * changed json fields to camel case * more changes * commit the rest of the cmd files, not sure if they even work * tweaks to docs * Deleted old api folder and rename the new one * Upgrade sqlc version to 1.30.0 in CI workflow --- .github/workflows/sqlc_ci.yml | 4 +- apps/api/Makefile | 15 +- apps/api/VERSION | 2 +- apps/api/cmd/BAT_worker/main.go | 41 +- apps/api/cmd/api/main.go | 114 +- apps/api/cmd/email_worker/main.go | 21 +- apps/api/cmd/samples/main.go | 44 + apps/api/docs/docs.go | 5624 ----------------- apps/api/docs/openapi.json | 1 + apps/api/docs/swagger.json | 5602 ---------------- apps/api/docs/swagger.yaml | 3650 ----------- apps/api/download-openapi.sh | 16 + apps/api/go.mod | 100 +- apps/api/go.sum | 203 +- apps/api/internal/README.md | 5 - apps/api/internal/api/api.go | 401 +- apps/api/internal/api/cookie/cookie.go | 33 + apps/api/internal/api/handlers/admissions.go | 145 - apps/api/internal/api/handlers/application.go | 731 --- apps/api/internal/api/handlers/auth.go | 197 - apps/api/internal/api/handlers/bat.go | 225 - apps/api/internal/api/handlers/discord.go | 111 - apps/api/internal/api/handlers/email.go | 164 - .../internal/api/handlers/event_interest.go | 88 - apps/api/internal/api/handlers/events.go | 961 --- apps/api/internal/api/handlers/handlers.go | 50 - apps/api/internal/api/handlers/redeemables.go | 257 - apps/api/internal/api/handlers/teams.go | 557 -- apps/api/internal/api/handlers/user.go | 281 - apps/api/internal/api/middleware/auth.go | 99 +- apps/api/internal/api/middleware/events.go | 109 - .../api/internal/api/middleware/middleware.go | 11 +- apps/api/internal/{ => api}/web/path.go | 0 apps/api/internal/{ => api}/web/query.go | 0 apps/api/internal/config/config.go | 16 +- apps/api/internal/cookie/cookie.go | 48 - apps/api/internal/ctxutils/event.go | 18 - apps/api/internal/ctxutils/user.go | 26 +- .../connection.go => database/database.go} | 4 +- apps/api/internal/database/errors.go | 47 + .../20260326160116_database_init.sql | 338 + .../{db => database}/queries/accounts.sql | 12 +- .../{db => database}/queries/applications.sql | 69 +- .../{db => database}/queries/bat_runs.sql | 24 +- .../queries/hackathons.sql} | 96 +- .../database/queries/interest_submissions.sql | 12 + .../{db => database}/queries/redeemables.sql | 26 +- .../{db => database}/queries/sessions.sql | 20 +- .../{db => database}/queries/stats.sql | 14 +- .../queries/team_invitations.sql | 0 .../queries/team_join_requests.sql | 8 +- .../{db => database}/queries/team_members.sql | 5 +- .../{db => database}/queries/teams.sql | 22 +- .../{db => database}/queries/users.sql | 46 +- .../{db => database}/repository/accounts.go | 41 +- .../database/repository/application.go | 137 + .../internal/database/repository/bat_runs.go | 67 + .../database/repository/event_interests.go | 31 + .../internal/database/repository/hackathon.go | 72 + .../database/repository/redeemables.go | 67 + .../{db => database}/repository/sessions.go | 16 +- .../repository/team_invitations.go | 10 +- .../database/repository/team_join_requests.go | 80 + .../database/repository/team_members.go | 58 + .../api/internal/database/repository/teams.go | 90 + .../api/internal/database/repository/users.go | 118 + .../{db => database}/sqlc/accounts.sql.go | 28 +- .../{db => database}/sqlc/applications.sql.go | 164 +- .../internal/database/sqlc/bat_runs.sql.go | 140 + apps/api/internal/{db => database}/sqlc/db.go | 2 +- .../internal/database/sqlc/hackathons.sql.go | 322 + .../database/sqlc/interest_submissions.sql.go | 43 + apps/api/internal/database/sqlc/models.go | 390 ++ .../{db => database}/sqlc/redeemables.sql.go | 81 +- .../{db => database}/sqlc/sessions.sql.go | 58 +- .../{db => database}/sqlc/stats.sql.go | 39 +- .../sqlc/team_invitations.sql.go | 20 +- .../sqlc/team_join_requests.sql.go | 82 +- .../{db => database}/sqlc/team_members.sql.go | 26 +- .../{db => database}/sqlc/teams.sql.go | 84 +- .../{db => database}/sqlc/users.sql.go | 195 +- .../internal/{db => database}/transaction.go | 2 +- apps/api/internal/db/errors.go | 20 - .../migrations/20250512145328_auth_init.sql | 105 - ..._remove_session_token_and_update_users.sql | 17 - ...20250608194039_add_roles_to_auth_users.sql | 12 - .../20250619161938_event_schema.sql | 57 - .../20250621222955_create_applications.sql | 39 - .../20250627064521_create_mailing_list.sql | 16 - ...0250825013123_remove_resume_url_column.sql | 9 - ...50825033231_update_application_trigger.sql | 27 - ...50905210705_add_preferred_email_column.sql | 11 - ...0250908055118_add_email_consent_column.sql | 11 - .../20250917044049_add_event_banner.sql | 11 - ...0251002000347_add_get_event_scope_type.sql | 13 - ...32904_submitted_by_column_applications.sql | 11 - .../migrations/20251101211753_teams_table.sql | 32 - .../20251106160823_saved_at_trigger.sql | 31 - ...11212010_invitations_and_join_requests.sql | 58 - .../20251121165836_add_app_review_columns.sql | 19 - ...7_add_application_waitlist_time_column.sql | 11 - .../20251215225937_add_bat_runs_schema.sql | 21 - ...200020_add_application_review_finished.sql | 11 - ...ove_application_review_finished_column.sql | 11 - .../20260116002956_checked_in_time.sql | 18 - ...260119015108_create_redeemables_tables.sql | 43 - .../db/queries/event_interest_submissions.sql | 12 - apps/api/internal/db/queries/event_roles.sql | 82 - .../api/internal/db/repository/application.go | 217 - apps/api/internal/db/repository/bat_runs.go | 74 - .../internal/db/repository/event_interest.go | 36 - apps/api/internal/db/repository/events.go | 227 - .../api/internal/db/repository/redeemables.go | 95 - .../db/repository/team_join_requests.go | 104 - .../internal/db/repository/team_members.go | 68 - apps/api/internal/db/repository/teams.go | 102 - apps/api/internal/db/repository/users.go | 97 - apps/api/internal/db/sqlc/bat_runs.sql.go | 158 - .../db/sqlc/event_interest_submissions.sql.go | 45 - apps/api/internal/db/sqlc/event_roles.sql.go | 374 -- apps/api/internal/db/sqlc/events.sql.go | 497 -- apps/api/internal/db/sqlc/models.go | 481 -- apps/api/internal/db/sqlc/querier.go | 55 - apps/api/internal/domains/application/http.go | 734 +++ .../application/service.go} | 478 +- apps/api/internal/domains/auth/http.go | 211 + .../auth.go => domains/auth/service.go} | 53 +- apps/api/internal/{ => domains}/bat/engine.go | 0 apps/api/internal/domains/bat/http.go | 134 + apps/api/internal/domains/bat/service.go | 266 + apps/api/internal/domains/email/http.go | 164 + apps/api/internal/domains/email/service.go | 296 + apps/api/internal/domains/hackathon/http.go | 456 ++ .../api/internal/domains/hackathon/service.go | 235 + apps/api/internal/domains/redeemables/http.go | 257 + .../redeemables/service.go} | 58 +- apps/api/internal/domains/teams/http.go | 445 ++ .../teams.go => domains/teams/service.go} | 197 +- apps/api/internal/domains/users/http.go | 508 ++ apps/api/internal/domains/users/service.go | 228 + .../api/internal/{email => emailutils}/ses.go | 2 +- .../templates/ApplicationAcceptedEmail.html | 0 .../templates/ApplicationRejectedEmail.html | 0 .../templates/ConfirmationEmail.html | 0 .../templates/WaitlistAcceptanceEmail.html | 0 .../templates/WelcomeEmail.html | 0 .../{email => emailutils}/validation.go | 2 +- apps/api/internal/parse/optional.go | 40 +- apps/api/internal/parse/parse.go | 46 - apps/api/internal/ptr/bool.go | 6 - apps/api/internal/ptr/int32.go | 5 - apps/api/internal/ptr/uuid.go | 8 - apps/api/internal/services/bat.go | 477 -- apps/api/internal/services/discord.go | 52 - apps/api/internal/services/email.go | 189 - apps/api/internal/services/event_interest.go | 47 - apps/api/internal/services/events.go | 434 -- apps/api/internal/services/user.go | 81 - apps/api/internal/tasks/bat.go | 5 +- apps/api/internal/workers/bat.go | 48 +- apps/api/internal/workers/email.go | 8 +- apps/api/sqlc.yml | 17 +- apps/docs/mkdocs.yml | 2 +- apps/docs/src/api/auth.md | 24 +- apps/docs/src/api/database.md | 188 +- apps/docs/src/api/index.md | 11 +- apps/docs/src/api/installation.md | 16 +- apps/docs/src/api/migrations.md | 40 +- apps/docs/src/api/openapi.md | 150 +- apps/docs/src/api/structure.md | 23 +- apps/docs/src/getting-started.md | 2 +- apps/docs/src/index.md | 6 +- apps/docs/src/repo-structure.md | 1 + apps/docs/src/workflow.md | 1 + 174 files changed, 7664 insertions(+), 25501 deletions(-) create mode 100644 apps/api/cmd/samples/main.go delete mode 100644 apps/api/docs/docs.go create mode 100644 apps/api/docs/openapi.json delete mode 100644 apps/api/docs/swagger.json delete mode 100644 apps/api/docs/swagger.yaml create mode 100755 apps/api/download-openapi.sh delete mode 100644 apps/api/internal/README.md create mode 100644 apps/api/internal/api/cookie/cookie.go delete mode 100644 apps/api/internal/api/handlers/admissions.go delete mode 100644 apps/api/internal/api/handlers/application.go delete mode 100644 apps/api/internal/api/handlers/auth.go delete mode 100644 apps/api/internal/api/handlers/bat.go delete mode 100644 apps/api/internal/api/handlers/discord.go delete mode 100644 apps/api/internal/api/handlers/email.go delete mode 100644 apps/api/internal/api/handlers/event_interest.go delete mode 100644 apps/api/internal/api/handlers/events.go delete mode 100644 apps/api/internal/api/handlers/handlers.go delete mode 100644 apps/api/internal/api/handlers/redeemables.go delete mode 100644 apps/api/internal/api/handlers/teams.go delete mode 100644 apps/api/internal/api/handlers/user.go delete mode 100644 apps/api/internal/api/middleware/events.go rename apps/api/internal/{ => api}/web/path.go (100%) rename apps/api/internal/{ => api}/web/query.go (100%) delete mode 100644 apps/api/internal/cookie/cookie.go delete mode 100644 apps/api/internal/ctxutils/event.go rename apps/api/internal/{db/connection.go => database/database.go} (82%) create mode 100644 apps/api/internal/database/errors.go create mode 100644 apps/api/internal/database/migrations/20260326160116_database_init.sql rename apps/api/internal/{db => database}/queries/accounts.sql (84%) rename apps/api/internal/{db => database}/queries/applications.sql (63%) rename apps/api/internal/{db => database}/queries/bat_runs.sql (65%) rename apps/api/internal/{db/queries/events.sql => database/queries/hackathons.sql} (53%) create mode 100644 apps/api/internal/database/queries/interest_submissions.sql rename apps/api/internal/{db => database}/queries/redeemables.sql (61%) rename apps/api/internal/{db => database}/queries/sessions.sql (65%) rename apps/api/internal/{db => database}/queries/stats.sql (91%) rename apps/api/internal/{db => database}/queries/team_invitations.sql (100%) rename apps/api/internal/{db => database}/queries/team_join_requests.sql (90%) rename apps/api/internal/{db => database}/queries/team_members.sql (85%) rename apps/api/internal/{db => database}/queries/teams.sql (81%) rename apps/api/internal/{db => database}/queries/users.sql (60%) rename apps/api/internal/{db => database}/repository/accounts.go (63%) create mode 100644 apps/api/internal/database/repository/application.go create mode 100644 apps/api/internal/database/repository/bat_runs.go create mode 100644 apps/api/internal/database/repository/event_interests.go create mode 100644 apps/api/internal/database/repository/hackathon.go create mode 100644 apps/api/internal/database/repository/redeemables.go rename apps/api/internal/{db => database}/repository/sessions.go (60%) rename apps/api/internal/{db => database}/repository/team_invitations.go (58%) create mode 100644 apps/api/internal/database/repository/team_join_requests.go create mode 100644 apps/api/internal/database/repository/team_members.go create mode 100644 apps/api/internal/database/repository/teams.go create mode 100644 apps/api/internal/database/repository/users.go rename apps/api/internal/{db => database}/sqlc/accounts.sql.go (91%) rename apps/api/internal/{db => database}/sqlc/applications.sql.go (57%) create mode 100644 apps/api/internal/database/sqlc/bat_runs.sql.go rename apps/api/internal/{db => database}/sqlc/db.go (96%) create mode 100644 apps/api/internal/database/sqlc/hackathons.sql.go create mode 100644 apps/api/internal/database/sqlc/interest_submissions.sql.go create mode 100644 apps/api/internal/database/sqlc/models.go rename apps/api/internal/{db => database}/sqlc/redeemables.sql.go (69%) rename apps/api/internal/{db => database}/sqlc/sessions.sql.go (78%) rename apps/api/internal/{db => database}/sqlc/stats.sql.go (81%) rename apps/api/internal/{db => database}/sqlc/team_invitations.sql.go (92%) rename apps/api/internal/{db => database}/sqlc/team_join_requests.sql.go (73%) rename apps/api/internal/{db => database}/sqlc/team_members.sql.go (79%) rename apps/api/internal/{db => database}/sqlc/teams.sql.go (64%) rename apps/api/internal/{db => database}/sqlc/users.sql.go (51%) rename apps/api/internal/{db => database}/transaction.go (98%) delete mode 100644 apps/api/internal/db/errors.go delete mode 100644 apps/api/internal/db/migrations/20250512145328_auth_init.sql delete mode 100644 apps/api/internal/db/migrations/20250608015747_remove_session_token_and_update_users.sql delete mode 100644 apps/api/internal/db/migrations/20250608194039_add_roles_to_auth_users.sql delete mode 100644 apps/api/internal/db/migrations/20250619161938_event_schema.sql delete mode 100644 apps/api/internal/db/migrations/20250621222955_create_applications.sql delete mode 100644 apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql delete mode 100644 apps/api/internal/db/migrations/20250825013123_remove_resume_url_column.sql delete mode 100644 apps/api/internal/db/migrations/20250825033231_update_application_trigger.sql delete mode 100644 apps/api/internal/db/migrations/20250905210705_add_preferred_email_column.sql delete mode 100644 apps/api/internal/db/migrations/20250908055118_add_email_consent_column.sql delete mode 100644 apps/api/internal/db/migrations/20250917044049_add_event_banner.sql delete mode 100644 apps/api/internal/db/migrations/20251002000347_add_get_event_scope_type.sql delete mode 100644 apps/api/internal/db/migrations/20251022032904_submitted_by_column_applications.sql delete mode 100644 apps/api/internal/db/migrations/20251101211753_teams_table.sql delete mode 100644 apps/api/internal/db/migrations/20251106160823_saved_at_trigger.sql delete mode 100644 apps/api/internal/db/migrations/20251111212010_invitations_and_join_requests.sql delete mode 100644 apps/api/internal/db/migrations/20251121165836_add_app_review_columns.sql delete mode 100644 apps/api/internal/db/migrations/20251208221807_add_application_waitlist_time_column.sql delete mode 100644 apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql delete mode 100644 apps/api/internal/db/migrations/20251216200020_add_application_review_finished.sql delete mode 100644 apps/api/internal/db/migrations/20251217065809_remove_application_review_finished_column.sql delete mode 100644 apps/api/internal/db/migrations/20260116002956_checked_in_time.sql delete mode 100644 apps/api/internal/db/migrations/20260119015108_create_redeemables_tables.sql delete mode 100644 apps/api/internal/db/queries/event_interest_submissions.sql delete mode 100644 apps/api/internal/db/queries/event_roles.sql delete mode 100644 apps/api/internal/db/repository/application.go delete mode 100644 apps/api/internal/db/repository/bat_runs.go delete mode 100644 apps/api/internal/db/repository/event_interest.go delete mode 100644 apps/api/internal/db/repository/events.go delete mode 100644 apps/api/internal/db/repository/redeemables.go delete mode 100644 apps/api/internal/db/repository/team_join_requests.go delete mode 100644 apps/api/internal/db/repository/team_members.go delete mode 100644 apps/api/internal/db/repository/teams.go delete mode 100644 apps/api/internal/db/repository/users.go delete mode 100644 apps/api/internal/db/sqlc/bat_runs.sql.go delete mode 100644 apps/api/internal/db/sqlc/event_interest_submissions.sql.go delete mode 100644 apps/api/internal/db/sqlc/event_roles.sql.go delete mode 100644 apps/api/internal/db/sqlc/events.sql.go delete mode 100644 apps/api/internal/db/sqlc/models.go delete mode 100644 apps/api/internal/db/sqlc/querier.go create mode 100644 apps/api/internal/domains/application/http.go rename apps/api/internal/{services/application.go => domains/application/service.go} (55%) create mode 100644 apps/api/internal/domains/auth/http.go rename apps/api/internal/{services/auth.go => domains/auth/service.go} (73%) rename apps/api/internal/{ => domains}/bat/engine.go (100%) create mode 100644 apps/api/internal/domains/bat/http.go create mode 100644 apps/api/internal/domains/bat/service.go create mode 100644 apps/api/internal/domains/email/http.go create mode 100644 apps/api/internal/domains/email/service.go create mode 100644 apps/api/internal/domains/hackathon/http.go create mode 100644 apps/api/internal/domains/hackathon/service.go create mode 100644 apps/api/internal/domains/redeemables/http.go rename apps/api/internal/{services/redeemables.go => domains/redeemables/service.go} (54%) create mode 100644 apps/api/internal/domains/teams/http.go rename apps/api/internal/{services/teams.go => domains/teams/service.go} (61%) create mode 100644 apps/api/internal/domains/users/http.go create mode 100644 apps/api/internal/domains/users/service.go rename apps/api/internal/{email => emailutils}/ses.go (99%) rename apps/api/internal/{email => emailutils}/templates/ApplicationAcceptedEmail.html (100%) rename apps/api/internal/{email => emailutils}/templates/ApplicationRejectedEmail.html (100%) rename apps/api/internal/{email => emailutils}/templates/ConfirmationEmail.html (100%) rename apps/api/internal/{email => emailutils}/templates/WaitlistAcceptanceEmail.html (100%) rename apps/api/internal/{email => emailutils}/templates/WelcomeEmail.html (100%) rename apps/api/internal/{email => emailutils}/validation.go (85%) delete mode 100644 apps/api/internal/parse/parse.go delete mode 100644 apps/api/internal/ptr/bool.go delete mode 100644 apps/api/internal/ptr/int32.go delete mode 100644 apps/api/internal/ptr/uuid.go delete mode 100644 apps/api/internal/services/bat.go delete mode 100644 apps/api/internal/services/discord.go delete mode 100644 apps/api/internal/services/email.go delete mode 100644 apps/api/internal/services/event_interest.go delete mode 100644 apps/api/internal/services/events.go delete mode 100644 apps/api/internal/services/user.go diff --git a/.github/workflows/sqlc_ci.yml b/.github/workflows/sqlc_ci.yml index 9f55463b..4f595874 100644 --- a/.github/workflows/sqlc_ci.yml +++ b/.github/workflows/sqlc_ci.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: sqlc-dev/setup-sqlc@v3 with: - sqlc-version: '1.29.0' + sqlc-version: '1.30.0' - run: sqlc diff vet: @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v4 - uses: sqlc-dev/setup-sqlc@v3 with: - sqlc-version: '1.29.0' + sqlc-version: '1.30.0' # Start a PostgreSQL server - uses: sqlc-dev/action-setup-postgres@master with: diff --git a/apps/api/Makefile b/apps/api/Makefile index b33b35bb..53e68610 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -1,24 +1,21 @@ include .env.dev migrate-up: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} up + @goose -dir ./internal/database/migrations postgres ${DATABASE_URL_MIGRATION} up migrate-down: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} down + @goose -dir ./internal/database/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 + @goose -dir ./internal/database/migrations postgres ${DATABASE_URL} up migrate-down-devcontainer: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL} down + @goose -dir ./internal/database/migrations postgres ${DATABASE_URL} down generate: @sqlc generate -openapi-generate: - swag init --dir cmd/api,internal/api/handlers --parseDependency --requiredByDefault -v3.1 - -openapi-format: - swag f --dir cmd/api,internal/api/handlers +generate-openapi: + chmod +x download-openapi.sh && ./download-openapi.sh diff --git a/apps/api/VERSION b/apps/api/VERSION index b82608c0..60453e69 100644 --- a/apps/api/VERSION +++ b/apps/api/VERSION @@ -1 +1 @@ -v0.1.0 +v1.0.0 \ No newline at end of file diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index e745ce78..ac98ca19 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -5,11 +5,15 @@ import ( "github.com/hibiken/asynq" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/email" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/domains/application" + "github.com/swamphacks/core/apps/api/internal/domains/bat" + "github.com/swamphacks/core/apps/api/internal/domains/email" + "github.com/swamphacks/core/apps/api/internal/domains/hackathon" + "github.com/swamphacks/core/apps/api/internal/emailutils" "github.com/swamphacks/core/apps/api/internal/logger" - "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/storage" "github.com/swamphacks/core/apps/api/internal/tasks" "github.com/swamphacks/core/apps/api/internal/workers" ) @@ -34,7 +38,7 @@ V V V }' `\ /' `{ V V V func main() { logger := logger.New() - cfg := config.Load() + cfg := config.LoadConfig() redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) if err != nil { @@ -70,23 +74,24 @@ func main() { taskQueueClient := asynq.NewClient(redisOpt) defer taskQueueClient.Close() - database := db.NewDB(cfg.DatabaseURL) - defer database.Close() + db := database.NewDB(cfg.DatabaseURL) + defer db.Close() - txm := db.NewTransactionManager(database) + txm := database.NewTransactionManager(db) - applicationRepo := repository.NewApplicationRepository(database) - eventRepo := repository.NewEventRespository(database) - userRepo := repository.NewUserRepository(database) - eventService := services.NewEventService(eventRepo, userRepo, nil, nil, logger) - batRunsRepo := repository.NewBatRunsRepository(database) + hackathonRepo := repository.NewHackathonRepository(db) + applicationRepo := repository.NewApplicationRepository(db) + userRepo := repository.NewUserRepository(db) + batRunsRepo := repository.NewBatRunsRepository(db) + eventInterestsRepo := repository.NewEventInterestsRepository(db) - sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - emailService := services.NewEmailService(taskQueueClient, sesClient, nil, logger) - batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, logger) - applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, nil, nil, scheduler, logger) + sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) + emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg) + batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, cfg, logger) + applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, nil, nil, scheduler, emailService, batService, cfg, logger) + hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, &storage.R2Client{}, nil, logger) - BATWorker := workers.NewBATWorker(batService, applicationService, eventService, scheduler, taskQueueClient, logger) + BATWorker := workers.NewBATWorker(batService, applicationService, hackathonService, scheduler, taskQueueClient, cfg, logger) mux := asynq.NewServeMux() mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask) diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index 1e427e1f..b30cd98b 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -1,121 +1,9 @@ package main import ( - "net/http" - "time" - - "github.com/hibiken/asynq" - "github.com/rs/zerolog/log" "github.com/swamphacks/core/apps/api/internal/api" - "github.com/swamphacks/core/apps/api/internal/api/handlers" - "github.com/swamphacks/core/apps/api/internal/api/middleware" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/logger" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/storage" ) -// @title SwampHacks Test API -// @version 1.0 -// @description This is SwampHacks' OpenAPI documentation. -// @termsOfService TODO - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { - logger := logger.New() - cfg := config.Load() - - // Init database connection and defer close - database := db.NewDB(cfg.DatabaseURL) - defer database.Close() - - // Create transaction manager - txm := db.NewTransactionManager(database) - - // Create injectable http client - client := &http.Client{ - Timeout: 10 * time.Second, - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - } - - // Create SES Client for email service - sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - - // Create asynq client - redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) - if err != nil { - logger.Fatal().Msg("Failed to parse REDIS_URL") - } - taskQueueClient := asynq.NewClient(redisOpt) - - // Create new middleware injectable - mw := middleware.NewMiddleware(database, logger, cfg) - - // Initialize storage clients - r2Client, err := storage.NewR2Client(cfg.CF.AccountID, cfg.CF.AccessKeyId, cfg.CF.AccessKeySecret, logger) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to create R2 client") - } - - // Injections into repositories - userRepo := repository.NewUserRepository(database) - accountRepo := repository.NewAccountRespository(database) - sessionRepo := repository.NewSessionRepository(database) - eventInterestRepo := repository.NewEventInterestRepository(database) - eventRepo := repository.NewEventRespository(database) - applicationRepo := repository.NewApplicationRepository(database) - teamRepo := repository.NewTeamRespository(database) - teamMemberRepo := repository.NewTeamMemberRespository(database) - teamJoinRequestRepo := repository.NewTeamJoinRequestRepository(database) - batRunsRepo := repository.NewBatRunsRepository(database) - redeemablesRepo := repository.NewRedeemablesRepository(database) - - // Injections into services - authService := services.NewAuthService(userRepo, accountRepo, sessionRepo, txm, client, logger, &cfg.Auth) - userService := services.NewUserService(userRepo, logger) - eventInterestService := services.NewEventInterestService(eventInterestRepo, logger) - eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger) - emailService := services.NewEmailService(taskQueueClient, sesClient, r2Client, logger) - applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, nil, logger) - teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger) - batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, logger) - redeemablesService := services.NewRedeemablesService(redeemablesRepo, logger) - discordService := services.NewDiscordService(eventRepo, logger) - - // Injections into handlers - apiHandlers := handlers.NewHandlers( - authService, - userService, - eventInterestService, - eventService, - emailService, - applicationService, - teamService, - batService, - redeemablesService, - discordService, - cfg, - logger, - ) - - api := api.NewAPI(&logger, apiHandlers, mw) - - logger.Info().Msgf("API listening on port %s", cfg.Port) - if err := http.ListenAndServe(":"+cfg.Port, api.Router); err != nil { - log.Fatal().Msg("Failed to start server.") - } + api.Run() } diff --git a/apps/api/cmd/email_worker/main.go b/apps/api/cmd/email_worker/main.go index 6ee0ae17..c04e1e01 100644 --- a/apps/api/cmd/email_worker/main.go +++ b/apps/api/cmd/email_worker/main.go @@ -7,16 +7,18 @@ import ( "github.com/hibiken/asynq" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/email" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/domains/email" + "github.com/swamphacks/core/apps/api/internal/emailutils" "github.com/swamphacks/core/apps/api/internal/logger" - "github.com/swamphacks/core/apps/api/internal/services" "github.com/swamphacks/core/apps/api/internal/tasks" "github.com/swamphacks/core/apps/api/internal/workers" ) func main() { logger := logger.New() - cfg := config.Load() + cfg := config.LoadConfig() redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) if err != nil { @@ -38,10 +40,19 @@ func main() { }, ) + taskQueueClient := asynq.NewClient(redisOpt) + defer taskQueueClient.Close() + + db := database.NewDB(cfg.DatabaseURL) + defer db.Close() + + hackathonRepo := repository.NewHackathonRepository(db) + userRepo := repository.NewUserRepository(db) + // Create ses client - sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) + sesClient := emailutils.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) - emailService := services.NewEmailService(nil, sesClient, nil, logger) + emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, nil, logger, cfg) emailWorker := workers.NewEmailWorker(emailService, logger) mux := asynq.NewServeMux() diff --git a/apps/api/cmd/samples/main.go b/apps/api/cmd/samples/main.go new file mode 100644 index 00000000..42edbe1c --- /dev/null +++ b/apps/api/cmd/samples/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func main() { + db := database.NewDB("postgres://postgres:postgres@localhost:5432/coredb") + defer db.Close() + + hackathonRepo := repository.NewHackathonRepository(db) + + appOpenTime := time.Date(2026, 3, 26, 19, 13, 20, 0, time.UTC) + appCloseTime := time.Date(2026, 4, 26, 19, 13, 20, 0, time.UTC) + startTime := time.Date(2026, 10, 26, 19, 13, 20, 0, time.UTC) + endTime := time.Date(2026, 10, 29, 19, 13, 20, 0, time.UTC) + + _, err := hackathonRepo.CreateHackathon(context.TODO(), sqlc.CreateHackathonParams{ + ID: "xii", + Name: "SwampHacks XII", + ApplicationOpen: appOpenTime, + ApplicationClose: appCloseTime, + StartTime: startTime, + EndTime: endTime, + Description: "SwampHacks' 12th iteration", + Location: "Reitz Union", + LocationUrl: nil, + MaxAttendees: nil, + RsvpDeadline: nil, + DecisionRelease: nil, + IsActive: true, + }) + + if err != nil { + fmt.Println("something went wrong") + fmt.Println(err) + } +} diff --git a/apps/api/docs/docs.go b/apps/api/docs/docs.go deleted file mode 100644 index f39bfe29..00000000 --- a/apps/api/docs/docs.go +++ /dev/null @@ -1,5624 +0,0 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - -package docs - -import "github.com/swaggo/swag/v2" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "components": { - "schemas": { - "handlers.AddEmailRequest": { - "properties": { - "email": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": [ - "email", - "source" - ], - "type": "object" - }, - "handlers.AssignRoleBatch": { - "properties": { - "assignments": { - "items": { - "$ref": "#/components/schemas/handlers.AssignRoleFields" - }, - "type": "array", - "uniqueItems": false - } - }, - "required": [ - "assignments" - ], - "type": "object" - }, - "handlers.AssignRoleFields": { - "properties": { - "email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "email", - "role", - "user_id" - ], - "type": "object" - }, - "handlers.CompleteOnboardingRequest": { - "properties": { - "name": { - "type": "string" - }, - "preferred_email": { - "type": "string" - } - }, - "required": [ - "name", - "preferred_email" - ], - "type": "object" - }, - "handlers.CreateEventFields": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "maxLength": 30, - "minLength": 5, - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "decision_release", - "description", - "end_time", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "website_url" - ], - "type": "object" - }, - "handlers.CreateJoinRequest": { - "properties": { - "message": { - "type": "string" - } - }, - "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": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "handlers.EventCheckInRequest": { - "properties": { - "rfid": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "rfid", - "user_id" - ], - "type": "object" - }, - "handlers.NullableEventRole": { - "properties": { - "assigned_at": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "assigned_at", - "event_id", - "role", - "user_id" - ], - "type": "object" - }, - "handlers.QueueConfirmationEmailFields": { - "properties": { - "email": { - "type": "string" - }, - "firstName": { - "type": "string" - } - }, - "required": [ - "email", - "firstName" - ], - "type": "object" - }, - "handlers.QueueTextEmailRequest": { - "properties": { - "body": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "to": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - } - }, - "required": [ - "body", - "subject", - "to" - ], - "type": "object" - }, - "handlers.ReviewRatings": { - "properties": { - "experience_rating": { - "maximum": 5, - "minimum": 1, - "type": "integer" - }, - "passion_rating": { - "maximum": 5, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "experience_rating", - "passion_rating" - ], - "type": "object" - }, - "handlers.UpdateEmailConsentRequest": { - "properties": { - "email_consent": { - "type": "boolean" - } - }, - "required": [ - "email_consent" - ], - "type": "object" - }, - "handlers.UpdateProfileRequest": { - "properties": { - "name": { - "type": "string" - }, - "preferred_email": { - "type": "string" - } - }, - "required": [ - "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": { - "email": { - "description": "Primary email address (nullable)", - "example": "user@example.com", - "type": "string" - }, - "emailConsent": { - "description": "Whether the user agreed to receive emails", - "example": false, - "type": "boolean" - }, - "image": { - "description": "Optional profile image URL", - "example": "https://cdn.example.com/avatar.png", - "nullable": true, - "type": "string" - }, - "name": { - "description": "Full display name", - "example": "Jane Doe", - "type": "string" - }, - "onboarded": { - "description": "Whether the user completed onboarding", - "example": true, - "type": "boolean" - }, - "preferredEmail": { - "description": "Preferred email address for communications", - "example": "user.alt@example.com", - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "userId": { - "description": "Unique identifier for the user", - "example": "550e8400-e29b-41d4-a716-446655440000", - "format": "uuid", - "type": "string" - } - }, - "required": [ - "email", - "emailConsent", - "image", - "name", - "onboarded", - "preferredEmail", - "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": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "error", - "message" - ], - "type": "object" - }, - "services.ApplicationReviewStatus": { - "enum": [ - "in_progress", - "completed" - ], - "type": "string", - "x-enum-varnames": [ - "ApplicationReviewStatusInProgress", - "ApplicationReviewStatusCompleted" - ] - }, - "services.ApplicationStatistics": { - "properties": { - "age_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationAgeSplitRow" - }, - "gender_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationGenderSplitRow" - }, - "major_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationMajorSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "race_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationRaceSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "school_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationSchoolSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "status_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationStatusSplitRow" - } - }, - "required": [ - "age_stats", - "gender_stats", - "major_stats", - "race_stats", - "school_stats", - "status_stats" - ], - "type": "object" - }, - "services.AssignedApplication": { - "properties": { - "status": { - "$ref": "#/components/schemas/services.ApplicationReviewStatus" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "status", - "user_id" - ], - "type": "object" - }, - "services.EventOverview": { - "properties": { - "application_status_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationStatusSplitRow" - }, - "application_submission_stats": { - "items": { - "$ref": "#/components/schemas/services.SubmissionTimesStatistics" - }, - "type": "array", - "uniqueItems": false - }, - "event_details": { - "$ref": "#/components/schemas/sqlc.Event" - } - }, - "required": [ - "application_status_stats", - "application_submission_stats", - "event_details" - ], - "type": "object" - }, - "services.MemberWithUserInfo": { - "properties": { - "email": { - "type": "string" - }, - "image": { - "type": "string" - }, - "joined_at": { - "type": "string" - }, - "name": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "email", - "image", - "joined_at", - "name", - "user_id" - ], - "type": "object" - }, - "services.ReviewerAssignment": { - "properties": { - "amount": { - "description": "Number of applications assigned (nil if autoassign)", - "type": "integer" - }, - "id": { - "description": "User/Reviewer ID", - "type": "string" - } - }, - "required": [ - "amount", - "id" - ], - "type": "object" - }, - "services.SubmissionTimesStatistics": { - "properties": { - "count": { - "type": "integer" - }, - "day": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "count", - "day" - ], - "type": "object" - }, - "services.TeamWithMembers": { - "properties": { - "event_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "members": { - "items": { - "$ref": "#/components/schemas/services.MemberWithUserInfo" - }, - "type": "array", - "uniqueItems": false - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string" - } - }, - "required": [ - "event_id", - "id", - "members", - "name", - "owner_id" - ], - "type": "object" - }, - "services.UserInfoForEvent": { - "properties": { - "checked_in_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "platform_role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "checked_in_at", - "email", - "event_role", - "image", - "name", - "platform_role", - "user_id" - ], - "type": "object" - }, - "sqlc.Application": { - "properties": { - "application": { - "items": { - "type": "integer" - }, - "type": "array", - "uniqueItems": false - }, - "assigned_reviewer_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "experience_rating": { - "type": "integer" - }, - "passion_rating": { - "type": "integer" - }, - "saved_at": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.NullApplicationStatus" - }, - "submitted_at": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "waitlist_join_time": { - "type": "string" - } - }, - "required": [ - "application", - "assigned_reviewer_id", - "created_at", - "event_id", - "experience_rating", - "passion_rating", - "saved_at", - "status", - "submitted_at", - "updated_at", - "user_id", - "waitlist_join_time" - ], - "type": "object" - }, - "sqlc.ApplicationStatus": { - "enum": [ - "started", - "submitted", - "under_review", - "accepted", - "rejected", - "waitlisted", - "withdrawn" - ], - "type": "string", - "x-enum-varnames": [ - "ApplicationStatusStarted", - "ApplicationStatusSubmitted", - "ApplicationStatusUnderReview", - "ApplicationStatusAccepted", - "ApplicationStatusRejected", - "ApplicationStatusWaitlisted", - "ApplicationStatusWithdrawn" - ] - }, - "sqlc.AuthUser": { - "properties": { - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "email_consent": { - "type": "boolean" - }, - "email_verified": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "onboarded": { - "type": "boolean" - }, - "preferred_email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "email", - "email_consent", - "email_verified", - "id", - "image", - "name", - "onboarded", - "preferred_email", - "role", - "updated_at" - ], - "type": "object" - }, - "sqlc.AuthUserRole": { - "description": "Role assigned to the user", - "enum": [ - "user", - "superuser" - ], - "type": "string", - "x-enum-varnames": [ - "AuthUserRoleUser", - "AuthUserRoleSuperuser" - ] - }, - "sqlc.BatRunStatus": { - "enum": [ - "running", - "completed", - "failed" - ], - "type": "string", - "x-enum-varnames": [ - "BatRunStatusRunning", - "BatRunStatusCompleted", - "BatRunStatusFailed" - ] - }, - "sqlc.Event": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "application_review_started": { - "type": "boolean" - }, - "banner": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "application_review_started", - "banner", - "created_at", - "decision_release", - "description", - "end_time", - "id", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "updated_at", - "website_url" - ], - "type": "object" - }, - "sqlc.EventRoleType": { - "enum": [ - "admin", - "staff", - "attendee", - "applicant" - ], - "type": "string", - "x-enum-varnames": [ - "EventRoleTypeAdmin", - "EventRoleTypeStaff", - "EventRoleTypeAttendee", - "EventRoleTypeApplicant" - ] - }, - "sqlc.GetApplicationAgeSplitRow": { - "properties": { - "age_18": { - "type": "integer" - }, - "age_19": { - "type": "integer" - }, - "age_20": { - "type": "integer" - }, - "age_21": { - "type": "integer" - }, - "age_22": { - "type": "integer" - }, - "age_23_plus": { - "type": "integer" - }, - "underage": { - "type": "integer" - } - }, - "required": [ - "age_18", - "age_19", - "age_20", - "age_21", - "age_22", - "age_23_plus", - "underage" - ], - "type": "object" - }, - "sqlc.GetApplicationGenderSplitRow": { - "properties": { - "female": { - "type": "integer" - }, - "male": { - "type": "integer" - }, - "non_binary": { - "type": "integer" - }, - "other": { - "type": "integer" - } - }, - "required": [ - "female", - "male", - "non_binary", - "other" - ], - "type": "object" - }, - "sqlc.GetApplicationMajorSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "major": { - "type": "string" - } - }, - "required": [ - "count", - "major" - ], - "type": "object" - }, - "sqlc.GetApplicationRaceSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "race_group": { - "type": "string" - } - }, - "required": [ - "count", - "race_group" - ], - "type": "object" - }, - "sqlc.GetApplicationSchoolSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "school": { - "type": "string" - } - }, - "required": [ - "count", - "school" - ], - "type": "object" - }, - "sqlc.GetApplicationStatusSplitRow": { - "properties": { - "accepted": { - "type": "integer" - }, - "rejected": { - "type": "integer" - }, - "started": { - "type": "integer" - }, - "submitted": { - "type": "integer" - }, - "under_review": { - "type": "integer" - }, - "waitlisted": { - "type": "integer" - }, - "withdrawn": { - "type": "integer" - } - }, - "required": [ - "accepted", - "rejected", - "started", - "submitted", - "under_review", - "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": { - "type": "string" - }, - "email": { - "type": "string" - }, - "email_consent": { - "type": "boolean" - }, - "email_verified": { - "type": "boolean" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "id": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "onboarded": { - "type": "boolean" - }, - "preferred_email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "email", - "email_consent", - "email_verified", - "event_role", - "id", - "image", - "name", - "onboarded", - "preferred_email", - "role", - "updated_at" - ], - "type": "object" - }, - "sqlc.GetEventsWithUserInfoRow": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "application_review_started": { - "type": "boolean" - }, - "application_status": { - "$ref": "#/components/schemas/sqlc.NullApplicationStatus" - }, - "banner": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.NullEventRoleType" - }, - "id": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "application_review_started", - "application_status", - "banner", - "created_at", - "decision_release", - "description", - "end_time", - "event_role", - "id", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "updated_at", - "website_url" - ], - "type": "object" - }, - "sqlc.GetRunsByEventIdRow": { - "properties": { - "accepted_applicants": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "completed_at": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "rejected_applicants": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "status": { - "$ref": "#/components/schemas/sqlc.NullBatRunStatus" - } - }, - "required": [ - "accepted_applicants", - "completed_at", - "created_at", - "id", - "rejected_applicants", - "status" - ], - "type": "object" - }, - "sqlc.JoinRequestStatus": { - "enum": [ - "PENDING", - "APPROVED", - "REJECTED" - ], - "type": "string", - "x-enum-varnames": [ - "JoinRequestStatusPENDING", - "JoinRequestStatusAPPROVED", - "JoinRequestStatusREJECTED" - ] - }, - "sqlc.ListJoinRequestsByTeamAndStatusWithUserRow": { - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "processed_at": { - "type": "string" - }, - "processed_by_user_id": { - "type": "string" - }, - "request_message": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.JoinRequestStatus" - }, - "team_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_email": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "user_image": { - "type": "string" - }, - "user_name": { - "type": "string" - } - }, - "required": [ - "created_at", - "id", - "processed_at", - "processed_by_user_id", - "request_message", - "status", - "team_id", - "updated_at", - "user_email", - "user_id", - "user_image", - "user_name" - ], - "type": "object" - }, - "sqlc.NullApplicationStatus": { - "properties": { - "application_status": { - "$ref": "#/components/schemas/sqlc.ApplicationStatus" - }, - "valid": { - "description": "Valid is true if ApplicationStatus is not NULL", - "type": "boolean" - } - }, - "required": [ - "application_status", - "valid" - ], - "type": "object" - }, - "sqlc.NullBatRunStatus": { - "properties": { - "bat_run_status": { - "$ref": "#/components/schemas/sqlc.BatRunStatus" - }, - "valid": { - "description": "Valid is true if BatRunStatus is not NULL", - "type": "boolean" - } - }, - "required": [ - "bat_run_status", - "valid" - ], - "type": "object" - }, - "sqlc.NullEventRoleType": { - "properties": { - "event_role_type": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "valid": { - "description": "Valid is true if EventRoleType is not NULL", - "type": "boolean" - } - }, - "required": [ - "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": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "event_id", - "id", - "name", - "owner_id", - "updated_at" - ], - "type": "object" - }, - "sqlc.TeamJoinRequest": { - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "processed_at": { - "type": "string" - }, - "processed_by_user_id": { - "type": "string" - }, - "request_message": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.JoinRequestStatus" - }, - "team_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "created_at", - "id", - "processed_at", - "processed_by_user_id", - "request_message", - "status", - "team_id", - "updated_at", - "user_id" - ], - "type": "object" - } - } - }, - "info": { - "contact": { - "email": "support@swagger.io", - "name": "API Support", - "url": "http://www.swagger.io/support" - }, - "description": "{{escape .Description}}", - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "termsOfService": "TODO", - "title": "{{.Title}}", - "version": "{{.Version}}" - }, - "externalDocs": { - "description": "", - "url": "" - }, - "paths": { - "/auth/callback": { - "post": { - "description": "This route is used for OAuth authentication methods to verify and login/create an account.", - "parameters": [ - { - "description": "The OAuth code passed back from the provider. Part of the PKCE flow.", - "in": "query", - "name": "code", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The state containing a base64 encoded version of the nonce, provider, and redirect url.", - "in": "query", - "name": "state", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The nonce for comparing against the callback state decoded to prevent CSRF attacks.", - "in": "header", - "name": "sh_auth_nonce", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK: User is logged in successfully", - "headers": { - "Set-Cookie": { - "description": "Sets a sh_session cookie to signify auth status", - "schema": { - "type": "string" - } - } - } - }, - "302": { - "description": "Found: Logged in and redirected to a requested location", - "headers": { - "Set-Cookie": { - "description": "Sets a sh_session cookie to signify auth status", - "schema": { - "type": "string" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Something went wrong with the request queries or their properties" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Something went wrong verifying identity or authenticating." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Gateway: Authenticating OAuth server did not respond or user does not exist" - } - }, - "summary": "OAuth2 Auth Callback", - "tags": [ - "Authentication" - ] - } - }, - "/auth/me": { - "get": { - "description": "Get the currently authenticated user's information.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/middleware.UserContext" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "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 a Confirmation Email request to the task queue", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.QueueConfirmationEmailFields", - "summary": "request", - "description": "Email data" - } - ] - } - } - }, - "description": "Email data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK: Email request queued" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request. The email request is potentially invalid." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: The server went kaput while queueing email sending" - } - }, - "summary": "Queue a Confirmation Email Request", - "tags": [ - "Email" - ] - } - }, - "/events": { - "get": { - "description": "Gets events with a nullable event role for authenticated users.", - "parameters": [ - { - "description": "Can be scoped to either published, scoped, or all. Scoped means admins and staff can see unpublished events", - "in": "query", - "name": "scope", - "schema": { - "default": "\"published\"", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventsWithUserInfoRow" - }, - "type": "array" - } - } - }, - "description": "OK: Events returned" - } - }, - "summary": "Get events", - "tags": [ - "Event" - ] - }, - "post": { - "description": "Create a new event with the provided details", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.CreateEventFields", - "summary": "request", - "description": "Event creation data" - } - ] - } - } - }, - "description": "Event creation data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Event" - } - } - }, - "description": "OK: Event created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "endTime is before startTime or applicationClose is before applicationOpen" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Create a new event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}": { - "delete": { - "description": "Delete an existing event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Event deleted" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Delete an event", - "tags": [ - "Event" - ] - }, - "get": { - "description": "Get a specific event by ID", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Event" - } - } - }, - "description": "OK - Event received" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get an event", - "tags": [ - "Event" - ] - }, - "patch": { - "description": "Update an existing event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Event updated (patched)" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Update an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/application": { - "get": { - "description": "Get the current user's application progress for an event. If this is their first time filling out the application, a new application will be created.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/sqlc.Application" - }, - { - "additionalProperties": {}, - "type": "object" - } - ] - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving application\"\\" - } - }, - "summary": "Get Current User's Application by Event ID", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/accept-acceptance": { - "patch": { - "description": "Sets event role to attendee, from applicant", - "parameters": [ - { - "description": "ID of the event to join the waitlist for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Acceptance successful" - }, - "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 accept" - } - }, - "summary": "Accept an acceptance after being accepted to an event.", - "tags": [ - "Application Event" - ] - } - }, - "/events/{eventId}/application/assign-reviewers": { - "post": { - "description": "Assigns applications for an event to reviewers for the application review process.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "items": { - "$ref": "#/components/schemas/services.ReviewerAssignment" - }, - "title": "request", - "type": "array" - } - ] - } - } - }, - "description": "Reviewer assignmnet payload", - "required": true - }, - "responses": { - "201": { - "description": "Reviewers assigned" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error assigning reviewers" - } - }, - "summary": "Assign application to reviewers", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/assigned": { - "get": { - "description": "Retrieves assigned applications and their review progress for the authenticated reviewer.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/services.AssignedApplication" - }, - "type": "array" - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving assigned application" - } - }, - "summary": "Get Assigned Application IDs and Progress", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/download-resume": { - "get": { - "description": "This handler creates a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error handling download resume request" - } - }, - "summary": "Download the user's uploaded resume from their event application", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/join-waitlist": { - "patch": { - "description": "Adds a waitlist join time to application. Sets status to waitlisted", - "parameters": [ - { - "description": "ID of the event to join the waitlist for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Event Waitlist joined 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 join waitlist" - } - }, - "summary": "Join event waitlist after rejected application status.", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/reset-reviews": { - "post": { - "description": "Resets all application reviews for a given event, clearing any existing reviewer assignments.", - "parameters": [ - { - "description": "ID of the event to reset reviews for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Application reviews reset 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 reset application reviews" - } - }, - "summary": "Reset application reviews", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/save": { - "post": { - "description": "Save user's progress on the application. File/Upload fields are not saved.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "title": "data", - "type": "object" - } - ] - } - } - }, - "description": "Form data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error saving application" - } - }, - "summary": "Save Application", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/stats": { - "get": { - "description": "This aggregates applications by race, gender, age, majors, and schools. This route is only available to event staff and admins.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.ApplicationStatistics" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting statistics" - } - }, - "summary": "Gets an event's submitted application statistics", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/submit": { - "post": { - "description": "Submit the application for an event.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "title": "formBody", - "type": "object" - } - } - }, - "description": "Submission form data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error submitting application" - } - }, - "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 withdraw acceptance from", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Acceptance 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 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}/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.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Application ID (Technically user ID)", - "in": "path", - "name": "applicationId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Application" - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving assigned application" - } - }, - "summary": "Get an application based on a user id and event id.", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/{applicationId}/resume": { - "get": { - "description": "This handler creates a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "The application ID (userId of applicant)", - "in": "path", - "name": "applicationId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error handling download resume request" - } - }, - "summary": "Get resume for application review", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/{applicationId}/review": { - "post": { - "description": "Handles ratings submissions from staff during the application review process.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.ReviewRatings", - "summary": "reviewData", - "description": "An object containing the passion and experience ratings" - } - } - }, - "description": "An object containing the passion and experience ratings", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "Created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error submitting application review" - } - }, - "summary": "Submit application review", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/bat-runs": { - "delete": { - "description": "Delete an existing BAT run", - "parameters": [ - { - "description": "Run ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Run deleted" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Delete a run", - "tags": [ - "Bat" - ] - }, - "get": { - "description": "Gets BatRuns.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetRunsByEventIdRow" - }, - "type": "array" - } - } - }, - "description": "OK: BatRuns returned" - } - }, - "summary": "Get BatRuns", - "tags": [ - "Bat" - ] - } - }, - "/events/{eventId}/checkin": { - "post": { - "description": "Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.EventCheckInRequest", - "summary": "request", - "description": "Event check in data" - } - } - }, - "description": "Event check in data", - "required": true - }, - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Malformed request body." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AddEmailRequest", - "summary": "request", - "description": "Interest submission data" - } - ] - } - } - }, - "description": "Interest submission data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK: Interest email created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Duplicate email found in DB" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Make an interest submission for an event (email list)", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/overview": { - "get": { - "description": "Returns data such as event details (name, description, location, dates, etc..) and basic application statistics", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.EventOverview" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting statistics" - } - }, - "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", - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Check if application reviews complete", - "tags": [ - "Bat" - ] - } - }, - "/events/{eventId}/role": { - "get": { - "description": "Get current user's role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.NullableEventRole" - } - } - }, - "description": "OK - Return role" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - Role not found" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - Role not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get the current user's event role for an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles": { - "post": { - "description": "Modify user's role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AssignRoleFields", - "summary": "request", - "description": "Event role data" - } - ] - } - } - }, - "description": "Event role data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Role updated" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Change or add event role of a user", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles/batch": { - "post": { - "description": "Modify users' role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AssignRoleBatch", - "summary": "request", - "description": "Event roles data" - } - ] - } - } - }, - "description": "Event roles data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Roles updated" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Change or add event role of a user in batch", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles/{userId}": { - "delete": { - "description": "Remove user's role for a specific event", - "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" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Role revoked" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Revoke event role of a user", - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventStaffRow" - }, - "type": "array" - } - } - }, - "description": "OK - Return users" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get all staff users for an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/teams": { - "get": { - "description": "Gets all teams for a specific event.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/services.TeamWithMembers" - }, - "type": "array" - } - } - }, - "description": "Teams successfully retrieved." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get an event's teams", - "tags": [ - "Team" - ] - }, - "post": { - "description": "Creates a new team for a specific event and assigns the creator as the owner.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.CreateTeamRequest", - "summary": "request", - "description": "Team Creation Payload" - } - ] - } - } - }, - "description": "Team Creation Payload", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Team" - } - } - }, - "description": "A team object" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request: you had request parameters needed for this method." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: You already have a team." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Create a new team", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/me": { - "get": { - "description": "Retrieves the team information and the full list of team members for the currently authenticated user within a specified event.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.TeamWithMembers" - } - } - }, - "description": "Team information and members successfully retrieved." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Team not found for the user in this event." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get the authenticated user's team and its members for this specific event.", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/me/pending-joins": { - "get": { - "description": "Retrieves the current user's pending requests for a specific event's teams.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.TeamJoinRequest" - }, - "type": "array" - } - } - }, - "description": "Successfully retrieved pending requests" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get your pending requests", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/{teamId}/join": { - "post": { - "description": "Requests to join a team or fails if user is already on a team.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.CreateJoinRequest", - "summary": "request", - "description": "Team Creation Payload" - } - } - }, - "description": "Team Creation Payload", - "required": true - }, - "responses": { - "204": { - "description": "Successfully left the team" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: User is already on a team." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Request to join a team", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/users": { - "get": { - "description": "Gets all users with any role for the event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventStaffRow" - }, - "type": "array" - } - } - }, - "description": "OK - Return users" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get all users for an event", - "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" - ] - } - }, - "/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": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting user info for event" - } - }, - "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", - "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.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the join request", - "in": "path", - "name": "request_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully accepted the join request" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found: The join request does not exist." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: The join request has already been responded to." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Accept a team join request", - "tags": [ - "Team" - ] - } - }, - "/teams/join/{requestId}/reject": { - "post": { - "description": "Rejects a pending team join request. Only the team owner can perform this action.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the join request", - "in": "path", - "name": "request_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully accepted the join request" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found: The join request does not exist." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: The join request has already been responded to." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Reject a team join request", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}": { - "get": { - "description": "Retrieves the team information and the full list of team members by a team id.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.TeamWithMembers" - } - } - }, - "description": "Team information and members successfully retrieved." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Team not found for the user in this event." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get a team and its members by team id.", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/members/me": { - "delete": { - "description": "Leaves a team if the requester is on the team. Depends on cookies for user retrieval.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully left the team" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Leave a team", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/members/{userId}": { - "delete": { - "description": "Kicks a member from a team. Only the team owner can perform this action.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the user to be kicked", - "in": "path", - "name": "userId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully kicked the team member" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Kick a member from a team", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/pending-joins": { - "get": { - "description": "Retrieves a team's pending join requests. This is only allowed for the team's owner.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.ListJoinRequestsByTeamAndStatusWithUserRow" - }, - "type": "array" - } - } - }, - "description": "Successfully retrieved pending requests" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get team's pending join requests", - "tags": [ - "Team" - ] - } - }, - "/users": { - "get": { - "description": "Get or search for users by name or email. If no search term is provided, returns all users with pagination.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Search term to filter users by name or email (optional)", - "in": "query", - "name": "search", - "schema": { - "type": "string" - } - }, - { - "description": "Maximum number of users to return (default is 50)", - "in": "query", - "name": "limit", - "schema": { - "maximum": 100, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Number of users to skip for pagination (default is 0)", - "in": "query", - "name": "offset", - "schema": { - "minimum": 0, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.AuthUser" - }, - "type": "array" - } - } - }, - "description": "OK: Returns a list of users matching the search criteria, or all users if no search term is provided." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid query parameter(s)" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to retrieve users" - } - }, - "summary": "Get/Search for users", - "tags": [ - "User" - ] - } - }, - "/users/email-consent": { - "patch": { - "description": "Update the user's email consent setting", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.UpdateEmailConsentRequest", - "summary": "request", - "description": "The update email consent request body" - } - } - }, - "description": "The update email consent request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to update email consent" - } - }, - "summary": "Update Email Consent", - "tags": [ - "User" - ] - } - }, - "/users/me": { - "get": { - "description": "Get profile information of the currently authenticated user.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.AuthUser" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User profile not found." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get User Profile", - "tags": [ - "User" - ] - }, - "patch": { - "description": "Update the user's information", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.UpdateProfileRequest", - "summary": "request", - "description": "The update profile request body" - } - } - }, - "description": "The update profile request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to update user profile" - } - }, - "summary": "Update User", - "tags": [ - "User" - ] - } - }, - "/users/me/onboarding": { - "patch": { - "description": "Onboard the user.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.CompleteOnboardingRequest", - "summary": "request", - "description": "The onboarding request body" - } - } - }, - "description": "The onboarding request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to complete onboarding" - } - }, - "summary": "Complete Onboarding", - "tags": [ - "User" - ] - } - } - }, - "openapi": "3.1.0" -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "1.0", - Title: "SwampHacks Test API", - Description: "This is SwampHacks' OpenAPI documentation.", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/apps/api/docs/openapi.json b/apps/api/docs/openapi.json new file mode 100644 index 00000000..c346a7c1 --- /dev/null +++ b/apps/api/docs/openapi.json @@ -0,0 +1 @@ +{"components":{"schemas":{"Application":{"additionalProperties":false,"properties":{"application":{"contentEncoding":"base64","type":"string"},"assigned_reviewer_id":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"experience_rating":{"format":"int32","type":["integer","null"]},"hackathon_iteration":{"type":"string"},"passion_rating":{"format":"int32","type":["integer","null"]},"saved_at":{"format":"date-time","type":"string"},"status":{"$ref":"#/components/schemas/NullApplicationStatus"},"submitted_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"},"waitlist_join_time":{"format":"date-time","type":["string","null"]}},"required":["user_id","status","application","created_at","saved_at","updated_at","submitted_at","experience_rating","passion_rating","assigned_reviewer_id","waitlist_join_time","hackathon_iteration"],"type":"object"},"ApplicationStatistics":{"additionalProperties":false,"properties":{"age_stats":{"$ref":"#/components/schemas/GetApplicationAgeSplitRow"},"gender_stats":{"$ref":"#/components/schemas/GetApplicationGenderSplitRow"},"major_stats":{"items":{"$ref":"#/components/schemas/GetApplicationMajorSplitRow"},"type":["array","null"]},"race_stats":{"items":{"$ref":"#/components/schemas/GetApplicationRaceSplitRow"},"type":["array","null"]},"school_stats":{"items":{"$ref":"#/components/schemas/GetApplicationSchoolSplitRow"},"type":["array","null"]},"status_stats":{"$ref":"#/components/schemas/GetApplicationStatusSplitRow"}},"required":["gender_stats","age_stats","race_stats","major_stats","school_stats","status_stats"],"type":"object"},"AssignRoleBatchRequest":{"additionalProperties":false,"properties":{"assignments":{"items":{"$ref":"#/components/schemas/AssignRoleRequest"},"type":["array","null"]}},"required":["assignments"],"type":"object"},"AssignRoleRequest":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"role":{"type":"string"},"user_id":{"type":["string","null"]}},"required":["email","user_id","role"],"type":"object"},"AssignedApplication":{"additionalProperties":false,"properties":{"applicantId":{"type":"string"},"status":{"type":"string"}},"required":["applicantId","status"],"type":"object"},"CheckInRequest":{"additionalProperties":false,"properties":{"rfid":{"type":["string","null"]},"user_id":{"type":"string"}},"required":["user_id","rfid"],"type":"object"},"CreateJoinRequest":{"additionalProperties":false,"properties":{"message":{"type":["string","null"]}},"required":["message"],"type":"object"},"CreateRedeemableRequest":{"additionalProperties":false,"properties":{"amount":{"format":"int64","minimum":1,"type":"integer"},"max_user_amount":{"format":"int64","type":"integer"},"name":{"minLength":1,"type":"string"}},"required":["name","amount","max_user_amount"],"type":"object"},"CreateTeamRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"GetApplicationAgeSplitRow":{"additionalProperties":false,"properties":{"age_18":{"format":"int64","type":"integer"},"age_19":{"format":"int64","type":"integer"},"age_20":{"format":"int64","type":"integer"},"age_21":{"format":"int64","type":"integer"},"age_22":{"format":"int64","type":"integer"},"age_23_plus":{"format":"int64","type":"integer"},"underage":{"format":"int64","type":"integer"}},"required":["underage","age_18","age_19","age_20","age_21","age_22","age_23_plus"],"type":"object"},"GetApplicationGenderSplitRow":{"additionalProperties":false,"properties":{"female":{"format":"int64","type":"integer"},"male":{"format":"int64","type":"integer"},"non_binary":{"format":"int64","type":"integer"},"other":{"format":"int64","type":"integer"}},"required":["male","female","non_binary","other"],"type":"object"},"GetApplicationMajorSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"major":{"type":"string"}},"required":["major","count"],"type":"object"},"GetApplicationRaceSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"race_group":{"type":"string"}},"required":["race_group","count"],"type":"object"},"GetApplicationSchoolSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"school":{"type":"string"}},"required":["school","count"],"type":"object"},"GetApplicationStatusSplitRow":{"additionalProperties":false,"properties":{"accepted":{"format":"int64","type":"integer"},"rejected":{"format":"int64","type":"integer"},"started":{"format":"int64","type":"integer"},"submitted":{"format":"int64","type":"integer"},"under_review":{"format":"int64","type":"integer"},"waitlisted":{"format":"int64","type":"integer"},"withdrawn":{"format":"int64","type":"integer"}},"required":["started","submitted","under_review","accepted","rejected","waitlisted","withdrawn"],"type":"object"},"GetAttendeesWithDiscordRow":{"additionalProperties":false,"properties":{"discord_id":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["discord_id","user_id","name","email"],"type":"object"},"GetRedeemablesRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"total_redeemed":{},"total_stock":{"format":"int32","type":"integer"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","total_stock","max_user_amount","created_at","updated_at","total_redeemed"],"type":"object"},"Hackathon":{"additionalProperties":false,"properties":{"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"application_review_started":{"type":"boolean"},"banner":{"type":["string","null"]},"created_at":{"format":"date-time","type":["string","null"]},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"is_published":{"type":["boolean","null"]},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"onerow_id":{"type":"boolean"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":["string","null"]},"website_url":{"type":["string","null"]}},"required":["name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","website_url","is_published","created_at","updated_at","banner","application_review_started","onerow_id"],"type":"object"},"ListJoinRequestsByTeamAndStatusWithUserRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_email":{"type":["string","null"]},"user_id":{"type":"string"},"user_image":{"type":["string","null"]},"user_name":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at","user_email","user_name","user_image"],"type":"object"},"MemberWithUserInfo":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"image":{"type":["string","null"]},"joined_at":{"format":"date-time","type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["user_id","email","image","name","joined_at"],"type":"object"},"NullApplicationStatus":{"additionalProperties":false,"properties":{"application_status":{"type":"string"},"valid":{"type":"boolean"}},"required":["application_status","valid"],"type":"object"},"OnboardingRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferred_email":{"type":"string"}},"required":["name","preferred_email"],"type":"object"},"Redeemable":{"additionalProperties":false,"properties":{"amount":{"format":"int32","type":"integer"},"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","amount","max_user_amount","created_at","updated_at"],"type":"object"},"ReviewRatings":{"additionalProperties":false,"properties":{"experience_rating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"},"passion_rating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"}},"required":["passion_rating","experience_rating"],"type":"object"},"ReviewerAssignment":{"additionalProperties":false,"properties":{"amount":{"format":"int64","type":["integer","null"]},"userId":{"type":"string"}},"required":["userId","amount"],"type":"object"},"SubmitInterestEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"source":{"type":["string","null"]}},"required":["email","source"],"type":"object"},"Team":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"name":{"type":"string"},"owner_id":{"type":"string"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","owner_id","created_at","updated_at"],"type":"object"},"TeamJoinRequest":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at"],"type":"object"},"TeamWithMembers":{"additionalProperties":false,"properties":{"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/MemberWithUserInfo"},"type":["array","null"]},"name":{"type":"string"},"owner_id":{"type":"string"}},"required":["id","owner_id","name","members"],"type":"object"},"UpdateEmailConsentRequest":{"additionalProperties":false,"properties":{"email_consent":{"type":"boolean"}},"required":["email_consent"],"type":"object"},"UpdateHackathonRequest":{"additionalProperties":false,"properties":{"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"is_published":{"type":"boolean"},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"website_url":{"type":["string","null"]}},"required":["name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","website_url","is_published"],"type":"object"},"UpdateRedeemableRequest":{"additionalProperties":false,"properties":{"max_user_amount":{"format":"int64","type":"integer"},"name":{"type":"string"},"total_stock":{"format":"int64","type":"integer"}},"type":"object"},"UpdateRedemptionRequest":{"additionalProperties":false,"properties":{"new_amount":{"format":"int64","type":"integer"}},"type":"object"},"UpdateUserRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferred_email":{"type":"string"}},"required":["name","preferred_email"],"type":"object"},"User":{"additionalProperties":false,"properties":{"checked_in_at":{"format":"date-time","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"email":{"type":["string","null"]},"email_consent":{"type":"boolean"},"email_verified":{"type":"boolean"},"id":{"type":"string"},"image":{"type":["string","null"]},"name":{"type":"string"},"onboarded":{"type":"boolean"},"preferred_email":{"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"type":"string"},"role_assigned_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","email","email_verified","onboarded","image","created_at","updated_at","preferred_email","email_consent","checked_in_at","rfid","role_assigned_at","role"],"type":"object"},"UserContext":{"additionalProperties":false,"properties":{"checkedInAt":{"format":"date-time","type":["string","null"]},"email":{"examples":["user@example.com"],"type":["string","null"]},"emailConsent":{"examples":[false],"type":"boolean"},"image":{"examples":["https://cdn.example.com/avatar.png"],"type":["string","null"]},"name":{"examples":["Jane Doe"],"type":"string"},"onboarded":{"examples":[true],"type":"boolean"},"preferredEmail":{"examples":["user.alt@example.com"],"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"enum":["admin","staff","attendee","applicant","visitor"],"type":"string"},"userId":{"examples":["550e8400-e29b-41d4-a716-446655440000"],"format":"uuid","type":"string"}},"required":["userId","email","preferredEmail","name","onboarded","image","role","emailConsent","rfid","checkedInAt"],"type":"object"}}},"info":{"title":"SwampHacks API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/application":{"get":{"description":"Get the application of the current user","operationId":"get-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application","tags":["Application"]}},"/application/accept-acceptance":{"patch":{"description":"Accept an acceptance after being accepted. Sets event role to attendee, from applicant.","operationId":"accept-application-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Application Acceptance","tags":["Application"]}},"/application/assigned":{"get":{"description":"Returns assigned applications and their review progress for the authenticated reviewer","operationId":"get-assigned-applications","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AssignedApplication"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Assigned Applications","tags":["Application"]}},"/application/calculate-admissions":{"post":{"description":"Queues an admission calculation task to the BAT worker","operationId":"calculate-admissions-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Admissions Calculation Request","tags":["Application"]}},"/application/join-waitlist":{"patch":{"description":"Adds a waitlist join time to application. Sets status to waitlisted","operationId":"join-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Join Waitlist","tags":["Application"]}},"/application/release-decisions/{runId}":{"post":{"description":"Releases decisions that were calculated by the worker from a specific run id","operationId":"release-decisions","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"runId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Release Decisions","tags":["Application"]}},"/application/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.","operationId":"get-download-resume-url","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume Download URL","tags":["Application"]}},"/application/review/assign":{"post":{"description":"Assigns applications to reviewers for the application review process.","operationId":"assign-application-reviewers","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ReviewerAssignment"},"type":["array","null"]}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Application Reviewers","tags":["Application"]}},"/application/review/reset":{"post":{"description":"Resets all application reviews, clearing any existing reviewer assignments.","operationId":"reset-application-reviews","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reset Application Reviews","tags":["Application"]}},"/application/review/{applicantId}":{"post":{"description":"Handles ratings submissions from staff during the application review process","operationId":"submit-application-review","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRatings"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application Review","tags":["Application"]}},"/application/review/{applicantId}/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.","operationId":"get-resume","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume URL (for review process)","tags":["Application"]}},"/application/save":{"post":{"description":"Save user's progress on the application. File/Upload fields are not saved (eg. resumes).","operationId":"save-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{}}}},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Save Application","tags":["Application"]}},"/application/stats":{"get":{"description":"Aggregates applications by race, gender, age, majors, and schools","operationId":"get-application-statistics","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationStatistics"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application Statistics","tags":["Application"]}},"/application/submit":{"post":{"description":"Submit the application","operationId":"submit-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application","tags":["Application"]}},"/application/transition-waitlisted-applications":{"patch":{"description":"Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.","operationId":"transition-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Transition Waitlisted Applications","tags":["Application"]}},"/application/withdraw-acceptance":{"patch":{"description":"Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.","operationId":"withdraw-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Acceptance","tags":["Application"]}},"/application/withdraw-attendance":{"patch":{"description":"Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.","operationId":"withdraw-attendance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Attendance","tags":["Application"]}},"/auth/callback":{"get":{"description":"Handles the OAuth provider callback, validates state and nonce, and sets the session cookie.","operationId":"oauth-callback","parameters":[{"description":"OAuth authorization code","explode":false,"in":"query","name":"code","required":true,"schema":{"description":"OAuth authorization code","type":"string"}},{"description":"Base64 encoded OAuth state","explode":false,"in":"query","name":"state","required":true,"schema":{"description":"Base64 encoded OAuth state","type":"string"}},{"description":"Auth nonce cookie for CSRF protection","in":"cookie","name":"sh_auth_nonce","required":true,"schema":{"description":"Auth nonce cookie for CSRF protection","type":"string"}},{"description":"Client user agent","in":"header","name":"User-Agent","schema":{"description":"Client user agent","type":"string"}}],"responses":{"204":{"description":"No Content","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"schema":{"type":"string"}}}},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"501":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Implemented"}},"summary":"OAuth Callback","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Logs out the authenticated user by invalidating their session","operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Logout","tags":["Auth"]}},"/hackathon":{"get":{"description":"Returns information of the hackathon","operationId":"get-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon","tags":["Hackathon"]},"patch":{"description":"Updates the information of the hackathon","operationId":"update-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHackathonRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Hackathon","tags":["Hackathon"]}},"/hackathon/attendees/count":{"get":{"description":"Returns the number of users who is attending the hackathon","operationId":"get-hackathon-attendees-count","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"format":"int64","type":"integer"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees Count","tags":["Hackathon"]}},"/hackathon/attendees/discord":{"get":{"description":"Returns all users with a discord account that is also attending the hackathon","operationId":"get-hackathon-attendees-with-discord","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetAttendeesWithDiscordRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees with Discord","tags":["Hackathon"]}},"/hackathon/attendees/userids":{"get":{"description":"Returns all users ids of users who are attending the hackathon","operationId":"get-hackathon-attendees-userids","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees User Ids","tags":["Hackathon"]}},"/hackathon/checkin":{"get":{"description":"Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.","operationId":"check-in","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckInRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Check In User","tags":["Hackathon"]}},"/hackathon/interest":{"post":{"description":"Submits an email to interest/mailing list for the hackathon","operationId":"submit-interest-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitInterestEmailRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Interest Email","tags":["Hackathon"]}},"/hackathon/staff":{"get":{"description":"Returns the users who are part of the current staff of the hackathon","operationId":"get-hackathon-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Staff","tags":["Hackathon"]}},"/redeemables":{"get":{"description":"Returns a list of all redeemable items","operationId":"get-redeemables","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetRedeemablesRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Redeemables","tags":["Redeemables"]},"post":{"description":"Creates a new redeemable item","operationId":"create-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRedeemableRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Redeemable"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}":{"delete":{"description":"Deletes a redeemable by id","operationId":"delete-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Redeemable","tags":["Redeemables"]},"patch":{"description":"Update specific fields (name, stock, max per user) of a redeemable","operationId":"update-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedeemableRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}/users/{userId}":{"patch":{"description":"Updates a redemption created by the user.","operationId":"update-redemption","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedemptionRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redemption","tags":["Redeemables"]},"post":{"description":"Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item","operationId":"redeem-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Redeem Redeemable","tags":["Redeemables"]}},"/teams":{"post":{"description":"Creates a new team and assigns the user as the owner. Returns the team.","operationId":"create-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Team","tags":["Team"]}},"/teams/me":{"get":{"description":"Returns the team information and the full list of team members for the currently authenticated user","operationId":"get-my-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get My Team","tags":["Team"]}},"/teams/me/pending-joins":{"get":{"description":"Returns the current user's pending requests for teams.","operationId":"get-my-pending-join-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TeamJoinRequest"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User's Pending Join Requests","tags":["Team"]}},"/teams/{requestId}/accept":{"post":{"description":"Accepts a pending team join request. Only the team owner can perform this action.","operationId":"accept-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Team Join Request","tags":["Team"]}},"/teams/{requestId}/reject":{"post":{"description":"Rejects a pending team join request. Only the team owner can perform this action.","operationId":"reject-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reject Team Join Request","tags":["Team"]}},"/teams/{teamId}":{"get":{"description":"Returns the team information and the full list of team members by team id","operationId":"get-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Team","tags":["Team"]}},"/teams/{teamId}/join":{"post":{"description":"Requests to join a team or fails if user is already on a team.","operationId":"create-join-team-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJoinRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinRequest"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Request to Join Team","tags":["Team"]}},"/teams/{teamId}/kick/{memberId}":{"post":{"description":"Kicks a member from a team. Only the team owner can perform this action.","operationId":"kick-member-from-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"memberId","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Kick Team Member","tags":["Team"]}},"/teams/{teamId}/leave":{"post":{"description":"Leaves a team if the user is on the team.","operationId":"leave-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Leave Team","tags":["Team"]}},"/teams/{teamId}/pending-joins":{"get":{"description":"Returns a team's pending join requests. This is only allowed for the team's owner.","operationId":"get-pending-join-team-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ListJoinRequestsByTeamAndStatusWithUserRow"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Forbidden"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Pending Join Requests for Team","tags":["Team"]}},"/users":{"get":{"description":"Get or search for users by name or email. If no search term is provided, returns all users with pagination.","operationId":"get-users","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"explode":false,"in":"query","name":"search","schema":{"type":"string"}},{"explode":false,"in":"query","name":"limit","schema":{"default":50,"format":"int64","type":"integer"}},{"explode":false,"in":"query","name":"offset","schema":{"default":0,"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Users","tags":["Users"]}},"/users/email/{email}":{"get":{"description":"Returns the user associated with the email","operationId":"get-user-by-email","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Email","tags":["Users"]}},"/users/me":{"get":{"description":"Returns the authenticated user's profile","operationId":"get-me","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserContext"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Me","tags":["Users"]},"patch":{"description":"Updates information of the authenticated user","operationId":"update-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update User","tags":["Users"]}},"/users/me/email-consent":{"patch":{"description":"Updates the user's email consent setting","operationId":"update-email-consent","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailConsentRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Email Consent","tags":["Users"]}},"/users/me/onboarding":{"patch":{"description":"Allows the user to submit information such as name and preferred email, and complete the onboarding process","operationId":"onboard-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Onboard User","tags":["Users"]}},"/users/rfid/{rfid}":{"get":{"description":"Returns the user associated with the RFID","operationId":"get-user-by-rfid","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rfid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By RFID","tags":["Users"]}},"/users/roles/assign":{"post":{"description":"Assigns/modify a user's role","operationId":"assign-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Role","tags":["Users"]}},"/users/roles/batch-assign":{"post":{"description":"Batch assign/modify multiple users' roles","operationId":"batch-assign-roles","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleBatchRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Batch Assign Roles","tags":["Users"]}},"/users/roles/revoke/{userId}":{"post":{"description":"Remove a user's role","operationId":"revoke-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Revoke Role","tags":["Users"]}},"/users/userid/{userId}":{"get":{"description":"Returns the user associated with the user id","operationId":"get-user-by-id","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Id","tags":["Users"]}}}} \ No newline at end of file diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json deleted file mode 100644 index 4a5d408a..00000000 --- a/apps/api/docs/swagger.json +++ /dev/null @@ -1,5602 +0,0 @@ -{ - "components": { - "schemas": { - "handlers.AddEmailRequest": { - "properties": { - "email": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": [ - "email", - "source" - ], - "type": "object" - }, - "handlers.AssignRoleBatch": { - "properties": { - "assignments": { - "items": { - "$ref": "#/components/schemas/handlers.AssignRoleFields" - }, - "type": "array", - "uniqueItems": false - } - }, - "required": [ - "assignments" - ], - "type": "object" - }, - "handlers.AssignRoleFields": { - "properties": { - "email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "email", - "role", - "user_id" - ], - "type": "object" - }, - "handlers.CompleteOnboardingRequest": { - "properties": { - "name": { - "type": "string" - }, - "preferred_email": { - "type": "string" - } - }, - "required": [ - "name", - "preferred_email" - ], - "type": "object" - }, - "handlers.CreateEventFields": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "maxLength": 30, - "minLength": 5, - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "decision_release", - "description", - "end_time", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "website_url" - ], - "type": "object" - }, - "handlers.CreateJoinRequest": { - "properties": { - "message": { - "type": "string" - } - }, - "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": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "handlers.EventCheckInRequest": { - "properties": { - "rfid": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "rfid", - "user_id" - ], - "type": "object" - }, - "handlers.NullableEventRole": { - "properties": { - "assigned_at": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "assigned_at", - "event_id", - "role", - "user_id" - ], - "type": "object" - }, - "handlers.QueueConfirmationEmailFields": { - "properties": { - "email": { - "type": "string" - }, - "firstName": { - "type": "string" - } - }, - "required": [ - "email", - "firstName" - ], - "type": "object" - }, - "handlers.QueueTextEmailRequest": { - "properties": { - "body": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "to": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - } - }, - "required": [ - "body", - "subject", - "to" - ], - "type": "object" - }, - "handlers.ReviewRatings": { - "properties": { - "experience_rating": { - "maximum": 5, - "minimum": 1, - "type": "integer" - }, - "passion_rating": { - "maximum": 5, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "experience_rating", - "passion_rating" - ], - "type": "object" - }, - "handlers.UpdateEmailConsentRequest": { - "properties": { - "email_consent": { - "type": "boolean" - } - }, - "required": [ - "email_consent" - ], - "type": "object" - }, - "handlers.UpdateProfileRequest": { - "properties": { - "name": { - "type": "string" - }, - "preferred_email": { - "type": "string" - } - }, - "required": [ - "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": { - "email": { - "description": "Primary email address (nullable)", - "example": "user@example.com", - "type": "string" - }, - "emailConsent": { - "description": "Whether the user agreed to receive emails", - "example": false, - "type": "boolean" - }, - "image": { - "description": "Optional profile image URL", - "example": "https://cdn.example.com/avatar.png", - "nullable": true, - "type": "string" - }, - "name": { - "description": "Full display name", - "example": "Jane Doe", - "type": "string" - }, - "onboarded": { - "description": "Whether the user completed onboarding", - "example": true, - "type": "boolean" - }, - "preferredEmail": { - "description": "Preferred email address for communications", - "example": "user.alt@example.com", - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "userId": { - "description": "Unique identifier for the user", - "example": "550e8400-e29b-41d4-a716-446655440000", - "format": "uuid", - "type": "string" - } - }, - "required": [ - "email", - "emailConsent", - "image", - "name", - "onboarded", - "preferredEmail", - "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": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "error", - "message" - ], - "type": "object" - }, - "services.ApplicationReviewStatus": { - "enum": [ - "in_progress", - "completed" - ], - "type": "string", - "x-enum-varnames": [ - "ApplicationReviewStatusInProgress", - "ApplicationReviewStatusCompleted" - ] - }, - "services.ApplicationStatistics": { - "properties": { - "age_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationAgeSplitRow" - }, - "gender_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationGenderSplitRow" - }, - "major_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationMajorSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "race_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationRaceSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "school_stats": { - "items": { - "$ref": "#/components/schemas/sqlc.GetApplicationSchoolSplitRow" - }, - "type": "array", - "uniqueItems": false - }, - "status_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationStatusSplitRow" - } - }, - "required": [ - "age_stats", - "gender_stats", - "major_stats", - "race_stats", - "school_stats", - "status_stats" - ], - "type": "object" - }, - "services.AssignedApplication": { - "properties": { - "status": { - "$ref": "#/components/schemas/services.ApplicationReviewStatus" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "status", - "user_id" - ], - "type": "object" - }, - "services.EventOverview": { - "properties": { - "application_status_stats": { - "$ref": "#/components/schemas/sqlc.GetApplicationStatusSplitRow" - }, - "application_submission_stats": { - "items": { - "$ref": "#/components/schemas/services.SubmissionTimesStatistics" - }, - "type": "array", - "uniqueItems": false - }, - "event_details": { - "$ref": "#/components/schemas/sqlc.Event" - } - }, - "required": [ - "application_status_stats", - "application_submission_stats", - "event_details" - ], - "type": "object" - }, - "services.MemberWithUserInfo": { - "properties": { - "email": { - "type": "string" - }, - "image": { - "type": "string" - }, - "joined_at": { - "type": "string" - }, - "name": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "email", - "image", - "joined_at", - "name", - "user_id" - ], - "type": "object" - }, - "services.ReviewerAssignment": { - "properties": { - "amount": { - "description": "Number of applications assigned (nil if autoassign)", - "type": "integer" - }, - "id": { - "description": "User/Reviewer ID", - "type": "string" - } - }, - "required": [ - "amount", - "id" - ], - "type": "object" - }, - "services.SubmissionTimesStatistics": { - "properties": { - "count": { - "type": "integer" - }, - "day": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "count", - "day" - ], - "type": "object" - }, - "services.TeamWithMembers": { - "properties": { - "event_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "members": { - "items": { - "$ref": "#/components/schemas/services.MemberWithUserInfo" - }, - "type": "array", - "uniqueItems": false - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string" - } - }, - "required": [ - "event_id", - "id", - "members", - "name", - "owner_id" - ], - "type": "object" - }, - "services.UserInfoForEvent": { - "properties": { - "checked_in_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "platform_role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "checked_in_at", - "email", - "event_role", - "image", - "name", - "platform_role", - "user_id" - ], - "type": "object" - }, - "sqlc.Application": { - "properties": { - "application": { - "items": { - "type": "integer" - }, - "type": "array", - "uniqueItems": false - }, - "assigned_reviewer_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "experience_rating": { - "type": "integer" - }, - "passion_rating": { - "type": "integer" - }, - "saved_at": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.NullApplicationStatus" - }, - "submitted_at": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "waitlist_join_time": { - "type": "string" - } - }, - "required": [ - "application", - "assigned_reviewer_id", - "created_at", - "event_id", - "experience_rating", - "passion_rating", - "saved_at", - "status", - "submitted_at", - "updated_at", - "user_id", - "waitlist_join_time" - ], - "type": "object" - }, - "sqlc.ApplicationStatus": { - "enum": [ - "started", - "submitted", - "under_review", - "accepted", - "rejected", - "waitlisted", - "withdrawn" - ], - "type": "string", - "x-enum-varnames": [ - "ApplicationStatusStarted", - "ApplicationStatusSubmitted", - "ApplicationStatusUnderReview", - "ApplicationStatusAccepted", - "ApplicationStatusRejected", - "ApplicationStatusWaitlisted", - "ApplicationStatusWithdrawn" - ] - }, - "sqlc.AuthUser": { - "properties": { - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "email_consent": { - "type": "boolean" - }, - "email_verified": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "onboarded": { - "type": "boolean" - }, - "preferred_email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "email", - "email_consent", - "email_verified", - "id", - "image", - "name", - "onboarded", - "preferred_email", - "role", - "updated_at" - ], - "type": "object" - }, - "sqlc.AuthUserRole": { - "description": "Role assigned to the user", - "enum": [ - "user", - "superuser" - ], - "type": "string", - "x-enum-varnames": [ - "AuthUserRoleUser", - "AuthUserRoleSuperuser" - ] - }, - "sqlc.BatRunStatus": { - "enum": [ - "running", - "completed", - "failed" - ], - "type": "string", - "x-enum-varnames": [ - "BatRunStatusRunning", - "BatRunStatusCompleted", - "BatRunStatusFailed" - ] - }, - "sqlc.Event": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "application_review_started": { - "type": "boolean" - }, - "banner": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "application_review_started", - "banner", - "created_at", - "decision_release", - "description", - "end_time", - "id", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "updated_at", - "website_url" - ], - "type": "object" - }, - "sqlc.EventRoleType": { - "enum": [ - "admin", - "staff", - "attendee", - "applicant" - ], - "type": "string", - "x-enum-varnames": [ - "EventRoleTypeAdmin", - "EventRoleTypeStaff", - "EventRoleTypeAttendee", - "EventRoleTypeApplicant" - ] - }, - "sqlc.GetApplicationAgeSplitRow": { - "properties": { - "age_18": { - "type": "integer" - }, - "age_19": { - "type": "integer" - }, - "age_20": { - "type": "integer" - }, - "age_21": { - "type": "integer" - }, - "age_22": { - "type": "integer" - }, - "age_23_plus": { - "type": "integer" - }, - "underage": { - "type": "integer" - } - }, - "required": [ - "age_18", - "age_19", - "age_20", - "age_21", - "age_22", - "age_23_plus", - "underage" - ], - "type": "object" - }, - "sqlc.GetApplicationGenderSplitRow": { - "properties": { - "female": { - "type": "integer" - }, - "male": { - "type": "integer" - }, - "non_binary": { - "type": "integer" - }, - "other": { - "type": "integer" - } - }, - "required": [ - "female", - "male", - "non_binary", - "other" - ], - "type": "object" - }, - "sqlc.GetApplicationMajorSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "major": { - "type": "string" - } - }, - "required": [ - "count", - "major" - ], - "type": "object" - }, - "sqlc.GetApplicationRaceSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "race_group": { - "type": "string" - } - }, - "required": [ - "count", - "race_group" - ], - "type": "object" - }, - "sqlc.GetApplicationSchoolSplitRow": { - "properties": { - "count": { - "type": "integer" - }, - "school": { - "type": "string" - } - }, - "required": [ - "count", - "school" - ], - "type": "object" - }, - "sqlc.GetApplicationStatusSplitRow": { - "properties": { - "accepted": { - "type": "integer" - }, - "rejected": { - "type": "integer" - }, - "started": { - "type": "integer" - }, - "submitted": { - "type": "integer" - }, - "under_review": { - "type": "integer" - }, - "waitlisted": { - "type": "integer" - }, - "withdrawn": { - "type": "integer" - } - }, - "required": [ - "accepted", - "rejected", - "started", - "submitted", - "under_review", - "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": { - "type": "string" - }, - "email": { - "type": "string" - }, - "email_consent": { - "type": "boolean" - }, - "email_verified": { - "type": "boolean" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "id": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "onboarded": { - "type": "boolean" - }, - "preferred_email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/sqlc.AuthUserRole" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "email", - "email_consent", - "email_verified", - "event_role", - "id", - "image", - "name", - "onboarded", - "preferred_email", - "role", - "updated_at" - ], - "type": "object" - }, - "sqlc.GetEventsWithUserInfoRow": { - "properties": { - "application_close": { - "type": "string" - }, - "application_open": { - "type": "string" - }, - "application_review_started": { - "type": "boolean" - }, - "application_status": { - "$ref": "#/components/schemas/sqlc.NullApplicationStatus" - }, - "banner": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "decision_release": { - "type": "string" - }, - "description": { - "type": "string" - }, - "end_time": { - "type": "string" - }, - "event_role": { - "$ref": "#/components/schemas/sqlc.NullEventRoleType" - }, - "id": { - "type": "string" - }, - "is_published": { - "type": "boolean" - }, - "location": { - "type": "string" - }, - "location_url": { - "type": "string" - }, - "max_attendees": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "rsvp_deadline": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "website_url": { - "type": "string" - } - }, - "required": [ - "application_close", - "application_open", - "application_review_started", - "application_status", - "banner", - "created_at", - "decision_release", - "description", - "end_time", - "event_role", - "id", - "is_published", - "location", - "location_url", - "max_attendees", - "name", - "rsvp_deadline", - "start_time", - "updated_at", - "website_url" - ], - "type": "object" - }, - "sqlc.GetRunsByEventIdRow": { - "properties": { - "accepted_applicants": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "completed_at": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "rejected_applicants": { - "items": { - "type": "string" - }, - "type": "array", - "uniqueItems": false - }, - "status": { - "$ref": "#/components/schemas/sqlc.NullBatRunStatus" - } - }, - "required": [ - "accepted_applicants", - "completed_at", - "created_at", - "id", - "rejected_applicants", - "status" - ], - "type": "object" - }, - "sqlc.JoinRequestStatus": { - "enum": [ - "PENDING", - "APPROVED", - "REJECTED" - ], - "type": "string", - "x-enum-varnames": [ - "JoinRequestStatusPENDING", - "JoinRequestStatusAPPROVED", - "JoinRequestStatusREJECTED" - ] - }, - "sqlc.ListJoinRequestsByTeamAndStatusWithUserRow": { - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "processed_at": { - "type": "string" - }, - "processed_by_user_id": { - "type": "string" - }, - "request_message": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.JoinRequestStatus" - }, - "team_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_email": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "user_image": { - "type": "string" - }, - "user_name": { - "type": "string" - } - }, - "required": [ - "created_at", - "id", - "processed_at", - "processed_by_user_id", - "request_message", - "status", - "team_id", - "updated_at", - "user_email", - "user_id", - "user_image", - "user_name" - ], - "type": "object" - }, - "sqlc.NullApplicationStatus": { - "properties": { - "application_status": { - "$ref": "#/components/schemas/sqlc.ApplicationStatus" - }, - "valid": { - "description": "Valid is true if ApplicationStatus is not NULL", - "type": "boolean" - } - }, - "required": [ - "application_status", - "valid" - ], - "type": "object" - }, - "sqlc.NullBatRunStatus": { - "properties": { - "bat_run_status": { - "$ref": "#/components/schemas/sqlc.BatRunStatus" - }, - "valid": { - "description": "Valid is true if BatRunStatus is not NULL", - "type": "boolean" - } - }, - "required": [ - "bat_run_status", - "valid" - ], - "type": "object" - }, - "sqlc.NullEventRoleType": { - "properties": { - "event_role_type": { - "$ref": "#/components/schemas/sqlc.EventRoleType" - }, - "valid": { - "description": "Valid is true if EventRoleType is not NULL", - "type": "boolean" - } - }, - "required": [ - "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": { - "type": "string" - }, - "event_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - }, - "required": [ - "created_at", - "event_id", - "id", - "name", - "owner_id", - "updated_at" - ], - "type": "object" - }, - "sqlc.TeamJoinRequest": { - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "processed_at": { - "type": "string" - }, - "processed_by_user_id": { - "type": "string" - }, - "request_message": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/sqlc.JoinRequestStatus" - }, - "team_id": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - } - }, - "required": [ - "created_at", - "id", - "processed_at", - "processed_by_user_id", - "request_message", - "status", - "team_id", - "updated_at", - "user_id" - ], - "type": "object" - } - } - }, - "info": { - "contact": { - "email": "support@swagger.io", - "name": "API Support", - "url": "http://www.swagger.io/support" - }, - "description": "This is SwampHacks' OpenAPI documentation.", - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "termsOfService": "TODO", - "title": "SwampHacks Test API", - "version": "1.0" - }, - "externalDocs": { - "description": "", - "url": "" - }, - "paths": { - "/auth/callback": { - "post": { - "description": "This route is used for OAuth authentication methods to verify and login/create an account.", - "parameters": [ - { - "description": "The OAuth code passed back from the provider. Part of the PKCE flow.", - "in": "query", - "name": "code", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The state containing a base64 encoded version of the nonce, provider, and redirect url.", - "in": "query", - "name": "state", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The nonce for comparing against the callback state decoded to prevent CSRF attacks.", - "in": "header", - "name": "sh_auth_nonce", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK: User is logged in successfully", - "headers": { - "Set-Cookie": { - "description": "Sets a sh_session cookie to signify auth status", - "schema": { - "type": "string" - } - } - } - }, - "302": { - "description": "Found: Logged in and redirected to a requested location", - "headers": { - "Set-Cookie": { - "description": "Sets a sh_session cookie to signify auth status", - "schema": { - "type": "string" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Something went wrong with the request queries or their properties" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Something went wrong verifying identity or authenticating." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Gateway: Authenticating OAuth server did not respond or user does not exist" - } - }, - "summary": "OAuth2 Auth Callback", - "tags": [ - "Authentication" - ] - } - }, - "/auth/me": { - "get": { - "description": "Get the currently authenticated user's information.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/middleware.UserContext" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "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 a Confirmation Email request to the task queue", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.QueueConfirmationEmailFields", - "summary": "request", - "description": "Email data" - } - ] - } - } - }, - "description": "Email data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK: Email request queued" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request. The email request is potentially invalid." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: The server went kaput while queueing email sending" - } - }, - "summary": "Queue a Confirmation Email Request", - "tags": [ - "Email" - ] - } - }, - "/events": { - "get": { - "description": "Gets events with a nullable event role for authenticated users.", - "parameters": [ - { - "description": "Can be scoped to either published, scoped, or all. Scoped means admins and staff can see unpublished events", - "in": "query", - "name": "scope", - "schema": { - "default": "\"published\"", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventsWithUserInfoRow" - }, - "type": "array" - } - } - }, - "description": "OK: Events returned" - } - }, - "summary": "Get events", - "tags": [ - "Event" - ] - }, - "post": { - "description": "Create a new event with the provided details", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.CreateEventFields", - "summary": "request", - "description": "Event creation data" - } - ] - } - } - }, - "description": "Event creation data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Event" - } - } - }, - "description": "OK: Event created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "endTime is before startTime or applicationClose is before applicationOpen" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Create a new event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}": { - "delete": { - "description": "Delete an existing event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Event deleted" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Delete an event", - "tags": [ - "Event" - ] - }, - "get": { - "description": "Get a specific event by ID", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Event" - } - } - }, - "description": "OK - Event received" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get an event", - "tags": [ - "Event" - ] - }, - "patch": { - "description": "Update an existing event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Event updated (patched)" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Update an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/application": { - "get": { - "description": "Get the current user's application progress for an event. If this is their first time filling out the application, a new application will be created.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/sqlc.Application" - }, - { - "additionalProperties": {}, - "type": "object" - } - ] - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving application\"\\" - } - }, - "summary": "Get Current User's Application by Event ID", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/accept-acceptance": { - "patch": { - "description": "Sets event role to attendee, from applicant", - "parameters": [ - { - "description": "ID of the event to join the waitlist for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Acceptance successful" - }, - "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 accept" - } - }, - "summary": "Accept an acceptance after being accepted to an event.", - "tags": [ - "Application Event" - ] - } - }, - "/events/{eventId}/application/assign-reviewers": { - "post": { - "description": "Assigns applications for an event to reviewers for the application review process.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "items": { - "$ref": "#/components/schemas/services.ReviewerAssignment" - }, - "title": "request", - "type": "array" - } - ] - } - } - }, - "description": "Reviewer assignmnet payload", - "required": true - }, - "responses": { - "201": { - "description": "Reviewers assigned" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error assigning reviewers" - } - }, - "summary": "Assign application to reviewers", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/assigned": { - "get": { - "description": "Retrieves assigned applications and their review progress for the authenticated reviewer.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/services.AssignedApplication" - }, - "type": "array" - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving assigned application" - } - }, - "summary": "Get Assigned Application IDs and Progress", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/download-resume": { - "get": { - "description": "This handler creates a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error handling download resume request" - } - }, - "summary": "Download the user's uploaded resume from their event application", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/join-waitlist": { - "patch": { - "description": "Adds a waitlist join time to application. Sets status to waitlisted", - "parameters": [ - { - "description": "ID of the event to join the waitlist for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Event Waitlist joined 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 join waitlist" - } - }, - "summary": "Join event waitlist after rejected application status.", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/reset-reviews": { - "post": { - "description": "Resets all application reviews for a given event, clearing any existing reviewer assignments.", - "parameters": [ - { - "description": "ID of the event to reset reviews for", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Application reviews reset 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 reset application reviews" - } - }, - "summary": "Reset application reviews", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/save": { - "post": { - "description": "Save user's progress on the application. File/Upload fields are not saved.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "title": "data", - "type": "object" - } - ] - } - } - }, - "description": "Form data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error saving application" - } - }, - "summary": "Save Application", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/stats": { - "get": { - "description": "This aggregates applications by race, gender, age, majors, and schools. This route is only available to event staff and admins.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.ApplicationStatistics" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting statistics" - } - }, - "summary": "Gets an event's submitted application statistics", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/submit": { - "post": { - "description": "Submit the application for an event.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "title": "formBody", - "type": "object" - } - } - }, - "description": "Submission form data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error submitting application" - } - }, - "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 withdraw acceptance from", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Acceptance 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 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}/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.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Application ID (Technically user ID)", - "in": "path", - "name": "applicationId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Application" - } - } - }, - "description": "OK: An application was found" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error retrieving assigned application" - } - }, - "summary": "Get an application based on a user id and event id.", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/{applicationId}/resume": { - "get": { - "description": "This handler creates a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "The application ID (userId of applicant)", - "in": "path", - "name": "applicationId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error handling download resume request" - } - }, - "summary": "Get resume for application review", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/application/{applicationId}/review": { - "post": { - "description": "Handles ratings submissions from staff during the application review process.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.ReviewRatings", - "summary": "reviewData", - "description": "An object containing the passion and experience ratings" - } - } - }, - "description": "An object containing the passion and experience ratings", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "Created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error submitting application review" - } - }, - "summary": "Submit application review", - "tags": [ - "Application" - ] - } - }, - "/events/{eventId}/bat-runs": { - "delete": { - "description": "Delete an existing BAT run", - "parameters": [ - { - "description": "Run ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "204": { - "description": "OK - Run deleted" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Delete a run", - "tags": [ - "Bat" - ] - }, - "get": { - "description": "Gets BatRuns.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetRunsByEventIdRow" - }, - "type": "array" - } - } - }, - "description": "OK: BatRuns returned" - } - }, - "summary": "Get BatRuns", - "tags": [ - "Bat" - ] - } - }, - "/events/{eventId}/checkin": { - "post": { - "description": "Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.EventCheckInRequest", - "summary": "request", - "description": "Event check in data" - } - } - }, - "description": "Event check in data", - "required": true - }, - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Malformed request body." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AddEmailRequest", - "summary": "request", - "description": "Interest submission data" - } - ] - } - } - }, - "description": "Interest submission data", - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "description": "OK: Interest email created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Duplicate email found in DB" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Make an interest submission for an event (email list)", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/overview": { - "get": { - "description": "Returns data such as event details (name, description, location, dates, etc..) and basic application statistics", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.EventOverview" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting statistics" - } - }, - "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", - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Check if application reviews complete", - "tags": [ - "Bat" - ] - } - }, - "/events/{eventId}/role": { - "get": { - "description": "Get current user's role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.NullableEventRole" - } - } - }, - "description": "OK - Return role" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - Role not found" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - Role not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get the current user's event role for an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles": { - "post": { - "description": "Modify user's role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AssignRoleFields", - "summary": "request", - "description": "Event role data" - } - ] - } - } - }, - "description": "Event role data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Role updated" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Change or add event role of a user", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles/batch": { - "post": { - "description": "Modify users' role for a specific event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.AssignRoleBatch", - "summary": "request", - "description": "Event roles data" - } - ] - } - } - }, - "description": "Event roles data", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Roles updated" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Change or add event role of a user in batch", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/roles/{userId}": { - "delete": { - "description": "Remove user's role for a specific event", - "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" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK - Role revoked" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found - User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Revoke event role of a user", - "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", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventStaffRow" - }, - "type": "array" - } - } - }, - "description": "OK - Return users" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get all staff users for an event", - "tags": [ - "Event" - ] - } - }, - "/events/{eventId}/teams": { - "get": { - "description": "Gets all teams for a specific event.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/services.TeamWithMembers" - }, - "type": "array" - } - } - }, - "description": "Teams successfully retrieved." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get an event's teams", - "tags": [ - "Team" - ] - }, - "post": { - "description": "Creates a new team for a specific event and assigns the creator as the owner.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/handlers.CreateTeamRequest", - "summary": "request", - "description": "Team Creation Payload" - } - ] - } - } - }, - "description": "Team Creation Payload", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.Team" - } - } - }, - "description": "A team object" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request: you had request parameters needed for this method." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: You already have a team." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Create a new team", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/me": { - "get": { - "description": "Retrieves the team information and the full list of team members for the currently authenticated user within a specified event.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.TeamWithMembers" - } - } - }, - "description": "Team information and members successfully retrieved." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Team not found for the user in this event." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get the authenticated user's team and its members for this specific event.", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/me/pending-joins": { - "get": { - "description": "Retrieves the current user's pending requests for a specific event's teams.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.TeamJoinRequest" - }, - "type": "array" - } - } - }, - "description": "Successfully retrieved pending requests" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get your pending requests", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/teams/{teamId}/join": { - "post": { - "description": "Requests to join a team or fails if user is already on a team.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the event", - "in": "path", - "name": "event_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.CreateJoinRequest", - "summary": "request", - "description": "Team Creation Payload" - } - } - }, - "description": "Team Creation Payload", - "required": true - }, - "responses": { - "204": { - "description": "Successfully left the team" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: User is already on a team." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Request to join a team", - "tags": [ - "Team" - ] - } - }, - "/events/{eventId}/users": { - "get": { - "description": "Gets all users with any role for the event", - "parameters": [ - { - "description": "Event ID", - "in": "path", - "name": "eventId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.GetEventStaffRow" - }, - "type": "array" - } - } - }, - "description": "OK - Return users" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: Something went terribly wrong on our end." - } - }, - "summary": "Get all users for an event", - "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" - ] - } - }, - "/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": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad request/Malformed request." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Server Error: error getting user info for event" - } - }, - "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", - "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.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the join request", - "in": "path", - "name": "request_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully accepted the join request" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found: The join request does not exist." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: The join request has already been responded to." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Accept a team join request", - "tags": [ - "Team" - ] - } - }, - "/teams/join/{requestId}/reject": { - "post": { - "description": "Rejects a pending team join request. Only the team owner can perform this action.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the join request", - "in": "path", - "name": "request_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully accepted the join request" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Not Found: The join request does not exist." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Conflict: The join request has already been responded to." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Reject a team join request", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}": { - "get": { - "description": "Retrieves the team information and the full list of team members by a team id.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/services.TeamWithMembers" - } - } - }, - "description": "Team information and members successfully retrieved." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Team not found for the user in this event." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get a team and its members by team id.", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/members/me": { - "delete": { - "description": "Leaves a team if the requester is on the team. Depends on cookies for user retrieval.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully left the team" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Leave a team", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/members/{userId}": { - "delete": { - "description": "Kicks a member from a team. Only the team owner can perform this action.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the user to be kicked", - "in": "path", - "name": "userId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Successfully kicked the team member" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went wrong." - } - }, - "summary": "Kick a member from a team", - "tags": [ - "Team" - ] - } - }, - "/teams/{teamId}/pending-joins": { - "get": { - "description": "Retrieves a team's pending join requests. This is only allowed for the team's owner.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session_id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "The ID of the team", - "in": "path", - "name": "team_id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.ListJoinRequestsByTeamAndStatusWithUserRow" - }, - "type": "array" - } - } - }, - "description": "Successfully retrieved pending requests" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Bad Request: Missing or malformed parameters." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Forbidden: Requester is not allowed to perform this action." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get team's pending join requests", - "tags": [ - "Team" - ] - } - }, - "/users": { - "get": { - "description": "Get or search for users by name or email. If no search term is provided, returns all users with pagination.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Search term to filter users by name or email (optional)", - "in": "query", - "name": "search", - "schema": { - "type": "string" - } - }, - { - "description": "Maximum number of users to return (default is 50)", - "in": "query", - "name": "limit", - "schema": { - "maximum": 100, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Number of users to skip for pagination (default is 0)", - "in": "query", - "name": "offset", - "schema": { - "minimum": 0, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/sqlc.AuthUser" - }, - "type": "array" - } - } - }, - "description": "OK: Returns a list of users matching the search criteria, or all users if no search term is provided." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid query parameter(s)" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to retrieve users" - } - }, - "summary": "Get/Search for users", - "tags": [ - "User" - ] - } - }, - "/users/email-consent": { - "patch": { - "description": "Update the user's email consent setting", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.UpdateEmailConsentRequest", - "summary": "request", - "description": "The update email consent request body" - } - } - }, - "description": "The update email consent request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to update email consent" - } - }, - "summary": "Update Email Consent", - "tags": [ - "User" - ] - } - }, - "/users/me": { - "get": { - "description": "Get profile information of the currently authenticated user.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/sqlc.AuthUser" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User profile not found." - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Something went seriously wrong." - } - }, - "summary": "Get User Profile", - "tags": [ - "User" - ] - }, - "patch": { - "description": "Update the user's information", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.UpdateProfileRequest", - "summary": "request", - "description": "The update profile request body" - } - } - }, - "description": "The update profile request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to update user profile" - } - }, - "summary": "Update User", - "tags": [ - "User" - ] - } - }, - "/users/me/onboarding": { - "patch": { - "description": "Onboard the user.", - "parameters": [ - { - "description": "The authenticated session token/id", - "in": "cookie", - "name": "sh_session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/handlers.CompleteOnboardingRequest", - "summary": "request", - "description": "The onboarding request body" - } - } - }, - "description": "The onboarding request body", - "required": true - }, - "responses": { - "200": { - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Invalid request body" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Unauthenticated: Requester is not currently authenticated." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "User not found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.ErrorResponse" - } - } - }, - "description": "Failed to complete onboarding" - } - }, - "summary": "Complete Onboarding", - "tags": [ - "User" - ] - } - } - }, - "openapi": "3.1.0" -} \ No newline at end of file diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml deleted file mode 100644 index bcfea44f..00000000 --- a/apps/api/docs/swagger.yaml +++ /dev/null @@ -1,3650 +0,0 @@ -components: - schemas: - handlers.AddEmailRequest: - properties: - email: - type: string - source: - type: string - required: - - email - - source - type: object - handlers.AssignRoleBatch: - properties: - assignments: - items: - $ref: '#/components/schemas/handlers.AssignRoleFields' - type: array - uniqueItems: false - required: - - assignments - type: object - handlers.AssignRoleFields: - properties: - email: - type: string - role: - $ref: '#/components/schemas/sqlc.EventRoleType' - user_id: - type: string - required: - - email - - role - - user_id - type: object - handlers.CompleteOnboardingRequest: - properties: - name: - type: string - preferred_email: - type: string - required: - - name - - preferred_email - type: object - handlers.CreateEventFields: - properties: - application_close: - type: string - application_open: - type: string - decision_release: - type: string - description: - type: string - end_time: - type: string - is_published: - type: boolean - location: - type: string - location_url: - type: string - max_attendees: - type: integer - name: - maxLength: 30 - minLength: 5 - type: string - rsvp_deadline: - type: string - start_time: - type: string - website_url: - type: string - required: - - application_close - - application_open - - decision_release - - description - - end_time - - is_published - - location - - location_url - - max_attendees - - name - - rsvp_deadline - - start_time - - website_url - type: object - handlers.CreateJoinRequest: - properties: - message: - type: string - 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: - type: string - required: - - name - type: object - handlers.EventCheckInRequest: - properties: - rfid: - type: string - user_id: - type: string - required: - - rfid - - user_id - type: object - handlers.NullableEventRole: - properties: - assigned_at: - type: string - event_id: - type: string - role: - $ref: '#/components/schemas/sqlc.EventRoleType' - user_id: - type: string - required: - - assigned_at - - event_id - - role - - user_id - type: object - handlers.QueueConfirmationEmailFields: - properties: - email: - type: string - firstName: - type: string - required: - - email - - firstName - type: object - handlers.QueueTextEmailRequest: - properties: - body: - type: string - subject: - type: string - to: - items: - type: string - type: array - uniqueItems: false - required: - - body - - subject - - to - type: object - handlers.ReviewRatings: - properties: - experience_rating: - maximum: 5 - minimum: 1 - type: integer - passion_rating: - maximum: 5 - minimum: 1 - type: integer - required: - - experience_rating - - passion_rating - type: object - handlers.UpdateEmailConsentRequest: - properties: - email_consent: - type: boolean - required: - - email_consent - type: object - handlers.UpdateProfileRequest: - properties: - name: - type: string - preferred_email: - type: string - required: - - 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: - email: - description: Primary email address (nullable) - example: user@example.com - type: string - emailConsent: - description: Whether the user agreed to receive emails - example: false - type: boolean - image: - description: Optional profile image URL - example: https://cdn.example.com/avatar.png - nullable: true - type: string - name: - description: Full display name - example: Jane Doe - type: string - onboarded: - description: Whether the user completed onboarding - example: true - type: boolean - preferredEmail: - description: Preferred email address for communications - example: user.alt@example.com - type: string - role: - $ref: '#/components/schemas/sqlc.AuthUserRole' - userId: - description: Unique identifier for the user - example: 550e8400-e29b-41d4-a716-446655440000 - format: uuid - type: string - required: - - email - - emailConsent - - image - - name - - onboarded - - preferredEmail - - 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: - type: string - message: - type: string - required: - - error - - message - type: object - services.ApplicationReviewStatus: - enum: - - in_progress - - completed - type: string - x-enum-varnames: - - ApplicationReviewStatusInProgress - - ApplicationReviewStatusCompleted - services.ApplicationStatistics: - properties: - age_stats: - $ref: '#/components/schemas/sqlc.GetApplicationAgeSplitRow' - gender_stats: - $ref: '#/components/schemas/sqlc.GetApplicationGenderSplitRow' - major_stats: - items: - $ref: '#/components/schemas/sqlc.GetApplicationMajorSplitRow' - type: array - uniqueItems: false - race_stats: - items: - $ref: '#/components/schemas/sqlc.GetApplicationRaceSplitRow' - type: array - uniqueItems: false - school_stats: - items: - $ref: '#/components/schemas/sqlc.GetApplicationSchoolSplitRow' - type: array - uniqueItems: false - status_stats: - $ref: '#/components/schemas/sqlc.GetApplicationStatusSplitRow' - required: - - age_stats - - gender_stats - - major_stats - - race_stats - - school_stats - - status_stats - type: object - services.AssignedApplication: - properties: - status: - $ref: '#/components/schemas/services.ApplicationReviewStatus' - user_id: - type: string - required: - - status - - user_id - type: object - services.EventOverview: - properties: - application_status_stats: - $ref: '#/components/schemas/sqlc.GetApplicationStatusSplitRow' - application_submission_stats: - items: - $ref: '#/components/schemas/services.SubmissionTimesStatistics' - type: array - uniqueItems: false - event_details: - $ref: '#/components/schemas/sqlc.Event' - required: - - application_status_stats - - application_submission_stats - - event_details - type: object - services.MemberWithUserInfo: - properties: - email: - type: string - image: - type: string - joined_at: - type: string - name: - type: string - user_id: - type: string - required: - - email - - image - - joined_at - - name - - user_id - type: object - services.ReviewerAssignment: - properties: - amount: - description: Number of applications assigned (nil if autoassign) - type: integer - id: - description: User/Reviewer ID - type: string - required: - - amount - - id - type: object - services.SubmissionTimesStatistics: - properties: - count: - type: integer - day: - format: date-time - type: string - required: - - count - - day - type: object - services.TeamWithMembers: - properties: - event_id: - type: string - id: - type: string - members: - items: - $ref: '#/components/schemas/services.MemberWithUserInfo' - type: array - uniqueItems: false - name: - type: string - owner_id: - type: string - required: - - event_id - - id - - members - - name - - owner_id - type: object - services.UserInfoForEvent: - properties: - checked_in_at: - type: string - email: - type: string - event_role: - $ref: '#/components/schemas/sqlc.EventRoleType' - image: - type: string - name: - type: string - platform_role: - $ref: '#/components/schemas/sqlc.AuthUserRole' - user_id: - type: string - required: - - checked_in_at - - email - - event_role - - image - - name - - platform_role - - user_id - type: object - sqlc.Application: - properties: - application: - items: - type: integer - type: array - uniqueItems: false - assigned_reviewer_id: - type: string - created_at: - type: string - event_id: - type: string - experience_rating: - type: integer - passion_rating: - type: integer - saved_at: - type: string - status: - $ref: '#/components/schemas/sqlc.NullApplicationStatus' - submitted_at: - type: string - updated_at: - type: string - user_id: - type: string - waitlist_join_time: - type: string - required: - - application - - assigned_reviewer_id - - created_at - - event_id - - experience_rating - - passion_rating - - saved_at - - status - - submitted_at - - updated_at - - user_id - - waitlist_join_time - type: object - sqlc.ApplicationStatus: - enum: - - started - - submitted - - under_review - - accepted - - rejected - - waitlisted - - withdrawn - type: string - x-enum-varnames: - - ApplicationStatusStarted - - ApplicationStatusSubmitted - - ApplicationStatusUnderReview - - ApplicationStatusAccepted - - ApplicationStatusRejected - - ApplicationStatusWaitlisted - - ApplicationStatusWithdrawn - sqlc.AuthUser: - properties: - created_at: - type: string - email: - type: string - email_consent: - type: boolean - email_verified: - type: boolean - id: - type: string - image: - type: string - name: - type: string - onboarded: - type: boolean - preferred_email: - type: string - role: - $ref: '#/components/schemas/sqlc.AuthUserRole' - updated_at: - type: string - required: - - created_at - - email - - email_consent - - email_verified - - id - - image - - name - - onboarded - - preferred_email - - role - - updated_at - type: object - sqlc.AuthUserRole: - description: Role assigned to the user - enum: - - user - - superuser - type: string - x-enum-varnames: - - AuthUserRoleUser - - AuthUserRoleSuperuser - sqlc.BatRunStatus: - enum: - - running - - completed - - failed - type: string - x-enum-varnames: - - BatRunStatusRunning - - BatRunStatusCompleted - - BatRunStatusFailed - sqlc.Event: - properties: - application_close: - type: string - application_open: - type: string - application_review_started: - type: boolean - banner: - type: string - created_at: - type: string - decision_release: - type: string - description: - type: string - end_time: - type: string - id: - type: string - is_published: - type: boolean - location: - type: string - location_url: - type: string - max_attendees: - type: integer - name: - type: string - rsvp_deadline: - type: string - start_time: - type: string - updated_at: - type: string - website_url: - type: string - required: - - application_close - - application_open - - application_review_started - - banner - - created_at - - decision_release - - description - - end_time - - id - - is_published - - location - - location_url - - max_attendees - - name - - rsvp_deadline - - start_time - - updated_at - - website_url - type: object - sqlc.EventRoleType: - enum: - - admin - - staff - - attendee - - applicant - type: string - x-enum-varnames: - - EventRoleTypeAdmin - - EventRoleTypeStaff - - EventRoleTypeAttendee - - EventRoleTypeApplicant - sqlc.GetApplicationAgeSplitRow: - properties: - age_18: - type: integer - age_19: - type: integer - age_20: - type: integer - age_21: - type: integer - age_22: - type: integer - age_23_plus: - type: integer - underage: - type: integer - required: - - age_18 - - age_19 - - age_20 - - age_21 - - age_22 - - age_23_plus - - underage - type: object - sqlc.GetApplicationGenderSplitRow: - properties: - female: - type: integer - male: - type: integer - non_binary: - type: integer - other: - type: integer - required: - - female - - male - - non_binary - - other - type: object - sqlc.GetApplicationMajorSplitRow: - properties: - count: - type: integer - major: - type: string - required: - - count - - major - type: object - sqlc.GetApplicationRaceSplitRow: - properties: - count: - type: integer - race_group: - type: string - required: - - count - - race_group - type: object - sqlc.GetApplicationSchoolSplitRow: - properties: - count: - type: integer - school: - type: string - required: - - count - - school - type: object - sqlc.GetApplicationStatusSplitRow: - properties: - accepted: - type: integer - rejected: - type: integer - started: - type: integer - submitted: - type: integer - under_review: - type: integer - waitlisted: - type: integer - withdrawn: - type: integer - required: - - accepted - - rejected - - started - - submitted - - under_review - - 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: - type: string - email: - type: string - email_consent: - type: boolean - email_verified: - type: boolean - event_role: - $ref: '#/components/schemas/sqlc.EventRoleType' - id: - type: string - image: - type: string - name: - type: string - onboarded: - type: boolean - preferred_email: - type: string - role: - $ref: '#/components/schemas/sqlc.AuthUserRole' - updated_at: - type: string - required: - - created_at - - email - - email_consent - - email_verified - - event_role - - id - - image - - name - - onboarded - - preferred_email - - role - - updated_at - type: object - sqlc.GetEventsWithUserInfoRow: - properties: - application_close: - type: string - application_open: - type: string - application_review_started: - type: boolean - application_status: - $ref: '#/components/schemas/sqlc.NullApplicationStatus' - banner: - type: string - created_at: - type: string - decision_release: - type: string - description: - type: string - end_time: - type: string - event_role: - $ref: '#/components/schemas/sqlc.NullEventRoleType' - id: - type: string - is_published: - type: boolean - location: - type: string - location_url: - type: string - max_attendees: - type: integer - name: - type: string - rsvp_deadline: - type: string - start_time: - type: string - updated_at: - type: string - website_url: - type: string - required: - - application_close - - application_open - - application_review_started - - application_status - - banner - - created_at - - decision_release - - description - - end_time - - event_role - - id - - is_published - - location - - location_url - - max_attendees - - name - - rsvp_deadline - - start_time - - updated_at - - website_url - type: object - sqlc.GetRunsByEventIdRow: - properties: - accepted_applicants: - items: - type: string - type: array - uniqueItems: false - completed_at: - type: string - created_at: - type: string - id: - type: string - rejected_applicants: - items: - type: string - type: array - uniqueItems: false - status: - $ref: '#/components/schemas/sqlc.NullBatRunStatus' - required: - - accepted_applicants - - completed_at - - created_at - - id - - rejected_applicants - - status - type: object - sqlc.JoinRequestStatus: - enum: - - PENDING - - APPROVED - - REJECTED - type: string - x-enum-varnames: - - JoinRequestStatusPENDING - - JoinRequestStatusAPPROVED - - JoinRequestStatusREJECTED - sqlc.ListJoinRequestsByTeamAndStatusWithUserRow: - properties: - created_at: - type: string - id: - type: string - processed_at: - type: string - processed_by_user_id: - type: string - request_message: - type: string - status: - $ref: '#/components/schemas/sqlc.JoinRequestStatus' - team_id: - type: string - updated_at: - type: string - user_email: - type: string - user_id: - type: string - user_image: - type: string - user_name: - type: string - required: - - created_at - - id - - processed_at - - processed_by_user_id - - request_message - - status - - team_id - - updated_at - - user_email - - user_id - - user_image - - user_name - type: object - sqlc.NullApplicationStatus: - properties: - application_status: - $ref: '#/components/schemas/sqlc.ApplicationStatus' - valid: - description: Valid is true if ApplicationStatus is not NULL - type: boolean - required: - - application_status - - valid - type: object - sqlc.NullBatRunStatus: - properties: - bat_run_status: - $ref: '#/components/schemas/sqlc.BatRunStatus' - valid: - description: Valid is true if BatRunStatus is not NULL - type: boolean - required: - - bat_run_status - - valid - type: object - sqlc.NullEventRoleType: - properties: - event_role_type: - $ref: '#/components/schemas/sqlc.EventRoleType' - valid: - description: Valid is true if EventRoleType is not NULL - type: boolean - required: - - 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: - type: string - event_id: - type: string - id: - type: string - name: - type: string - owner_id: - type: string - updated_at: - type: string - required: - - created_at - - event_id - - id - - name - - owner_id - - updated_at - type: object - sqlc.TeamJoinRequest: - properties: - created_at: - type: string - id: - type: string - processed_at: - type: string - processed_by_user_id: - type: string - request_message: - type: string - status: - $ref: '#/components/schemas/sqlc.JoinRequestStatus' - team_id: - type: string - updated_at: - type: string - user_id: - type: string - required: - - created_at - - id - - processed_at - - processed_by_user_id - - request_message - - status - - team_id - - updated_at - - user_id - type: object -externalDocs: - description: "" - url: "" -info: - contact: - email: support@swagger.io - name: API Support - url: http://www.swagger.io/support - description: This is SwampHacks' OpenAPI documentation. - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - termsOfService: TODO - title: SwampHacks Test API - version: "1.0" -openapi: 3.1.0 -paths: - /auth/callback: - post: - description: This route is used for OAuth authentication methods to verify and - login/create an account. - parameters: - - description: The OAuth code passed back from the provider. Part of the PKCE - flow. - in: query - name: code - required: true - schema: - type: string - - description: The state containing a base64 encoded version of the nonce, provider, - and redirect url. - in: query - name: state - required: true - schema: - type: string - - description: The nonce for comparing against the callback state decoded to - prevent CSRF attacks. - in: header - name: sh_auth_nonce - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - type: object - description: 'OK: User is logged in successfully' - headers: - Set-Cookie: - description: Sets a sh_session cookie to signify auth status - schema: - type: string - "302": - description: 'Found: Logged in and redirected to a requested location' - headers: - Set-Cookie: - description: Sets a sh_session cookie to signify auth status - schema: - type: string - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Something went wrong with the request queries - or their properties' - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Forbidden: Something went wrong verifying identity or authenticating.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Internal Server Error - "502": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Gateway: Authenticating OAuth server did not respond or - user does not exist' - summary: OAuth2 Auth Callback - tags: - - Authentication - /auth/me: - get: - description: Get the currently authenticated user's information. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/middleware.UserContext' - description: OK - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Internal Server Error - 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 a Confirmation Email request to the task queue - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.QueueConfirmationEmailFields' - description: Email data - summary: request - description: Email data - required: true - responses: - "201": - content: - application/json: - schema: - type: string - description: 'OK: Email request queued' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. The email request is potentially - invalid. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: The server went kaput while queueing email sending' - summary: Queue a Confirmation Email Request - tags: - - Email - /events: - get: - description: Gets events with a nullable event role for authenticated users. - parameters: - - description: Can be scoped to either published, scoped, or all. Scoped means - admins and staff can see unpublished events - in: query - name: scope - schema: - default: '"published"' - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.GetEventsWithUserInfoRow' - type: array - description: 'OK: Events returned' - summary: Get events - tags: - - Event - post: - description: Create a new event with the provided details - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.CreateEventFields' - description: Event creation data - summary: request - description: Event creation data - required: true - responses: - "201": - content: - application/json: - schema: - $ref: '#/components/schemas/sqlc.Event' - description: 'OK: Event created' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: endTime is before startTime or applicationClose is before applicationOpen - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Create a new event - tags: - - Event - /events/{eventId}: - delete: - description: Delete an existing event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "204": - description: OK - Event deleted - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Delete an event - tags: - - Event - get: - description: Get a specific event by ID - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "201": - content: - application/json: - schema: - $ref: '#/components/schemas/sqlc.Event' - description: OK - Event received - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Get an event - tags: - - Event - patch: - description: Update an existing event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "204": - description: OK - Event updated (patched) - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Update an event - tags: - - Event - /events/{eventId}/application: - get: - description: Get the current user's application progress for an event. If this - is their first time filling out the application, a new application will be - created. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - type: string - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/sqlc.Application' - - additionalProperties: {} - type: object - description: 'OK: An application was found' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error retrieving application"\' - summary: Get Current User's Application by Event ID - 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. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - type: string - - description: Application ID (Technically user ID) - in: path - name: applicationId - required: true - schema: - type: string - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/sqlc.Application' - description: 'OK: An application was found' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error retrieving assigned application' - summary: Get an application based on a user id and event id. - tags: - - Application - /events/{eventId}/application/{applicationId}/resume: - get: - description: This handler creates a presigned S3 URL with GET permission for - a specific user's resume as an object. The client can use this URL to download - the object temporarily for application review. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - - description: The application ID (userId of applicant) - in: path - name: applicationId - required: true - schema: - format: uuid - type: string - responses: - "200": - content: - application/json: - schema: - type: string - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error handling download resume request' - summary: Get resume for application review - tags: - - Application - /events/{eventId}/application/{applicationId}/review: - post: - description: Handles ratings submissions from staff during the application review - process. - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.ReviewRatings' - description: An object containing the passion and experience ratings - summary: reviewData - description: An object containing the passion and experience ratings - required: true - responses: - "201": - content: - application/json: - schema: - type: object - description: Created - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error submitting application review' - summary: Submit application review - tags: - - Application - /events/{eventId}/application/accept-acceptance: - patch: - description: Sets event role to attendee, from applicant - parameters: - - description: ID of the event to join the waitlist for - in: path - name: eventId - required: true - schema: - type: string - responses: - "200": - description: Acceptance successful - "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 accept' - summary: Accept an acceptance after being accepted to an event. - tags: - - Application Event - /events/{eventId}/application/assign-reviewers: - post: - description: Assigns applications for an event to reviewers for the application - review process. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - items: - $ref: '#/components/schemas/services.ReviewerAssignment' - title: request - type: array - description: Reviewer assignmnet payload - required: true - responses: - "201": - description: Reviewers assigned - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error assigning reviewers' - summary: Assign application to reviewers - tags: - - Application - /events/{eventId}/application/assigned: - get: - description: Retrieves assigned applications and their review progress for the - authenticated reviewer. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - type: string - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/services.AssignedApplication' - type: array - description: 'OK: An application was found' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error retrieving assigned application' - summary: Get Assigned Application IDs and Progress - tags: - - Application - /events/{eventId}/application/download-resume: - get: - description: This handler creates a presigned S3 URL with GET permission for - the user's specific object, which is their uploaded resume. The client can - use this URL to download the object. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - responses: - "200": - content: - application/json: - schema: - type: string - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error handling download resume request' - summary: Download the user's uploaded resume from their event application - tags: - - Application - /events/{eventId}/application/join-waitlist: - patch: - description: Adds a waitlist join time to application. Sets status to waitlisted - parameters: - - description: ID of the event to join the waitlist for - in: path - name: eventId - required: true - schema: - type: string - responses: - "200": - description: Event Waitlist joined 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 join waitlist' - summary: Join event waitlist after rejected application status. - tags: - - Application - /events/{eventId}/application/reset-reviews: - post: - description: Resets all application reviews for a given event, clearing any - existing reviewer assignments. - parameters: - - description: ID of the event to reset reviews for - in: path - name: eventId - required: true - schema: - type: string - responses: - "200": - description: Application reviews reset 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 reset application reviews' - summary: Reset application reviews - tags: - - Application - /events/{eventId}/application/save: - post: - description: Save user's progress on the application. File/Upload fields are - not saved. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - title: data - type: object - description: Form data - required: true - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error saving application' - summary: Save Application - tags: - - Application - /events/{eventId}/application/stats: - get: - description: This aggregates applications by race, gender, age, majors, and - schools. This route is only available to event staff and admins. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/services.ApplicationStatistics' - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error getting statistics' - summary: Gets an event's submitted application statistics - tags: - - Application - /events/{eventId}/application/submit: - post: - description: Submit the application for an event. - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - application/x-www-form-urlencoded: - schema: - title: formBody - type: object - description: Submission form data - required: true - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error submitting application' - 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 withdraw acceptance from - in: path - name: eventId - required: true - schema: - type: string - responses: - "200": - description: Acceptance 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 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 BAT run - parameters: - - description: Run ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "204": - description: OK - Run deleted - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Delete a run - tags: - - Bat - get: - description: Gets BatRuns. - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.GetRunsByEventIdRow' - type: array - description: 'OK: BatRuns returned' - summary: Get BatRuns - tags: - - Bat - /events/{eventId}/checkin: - post: - description: Staff route for checking a user to an event. The user to check - in must be an attendee and have never been checked in yet. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the event - in: path - name: event_id - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.EventCheckInRequest' - description: Event check in data - summary: request - description: Event check in data - required: true - responses: - "204": - description: No Content - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Malformed request body. - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - 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 - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.AddEmailRequest' - description: Interest submission data - summary: request - description: Interest submission data - required: true - responses: - "201": - content: - application/json: - schema: - type: string - description: 'OK: Interest email created' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Duplicate email found in DB - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Make an interest submission for an event (email list) - tags: - - Event - /events/{eventId}/overview: - get: - description: Returns data such as event details (name, description, location, - dates, etc..) and basic application statistics - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/services.EventOverview' - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error getting statistics' - 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 - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Check if application reviews complete - tags: - - Bat - /events/{eventId}/role: - get: - description: Get current user's role for a specific event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.NullableEventRole' - description: OK - Return role - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Not Found - Role not found - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Not Found - Role not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Get the current user's event role for an event - tags: - - Event - /events/{eventId}/roles: - post: - description: Modify user's role for a specific event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.AssignRoleFields' - description: Event role data - summary: request - description: Event role data - required: true - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - Role updated - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Not Found - User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Change or add event role of a user - tags: - - Event - /events/{eventId}/roles/{userId}: - delete: - description: Remove user's role for a specific event - 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 - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - Role revoked - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Not Found - User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Revoke event role of a user - tags: - - Event - /events/{eventId}/roles/batch: - post: - description: Modify users' role for a specific event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.AssignRoleBatch' - description: Event roles data - summary: request - description: Event roles data - required: true - responses: - "200": - content: - application/json: - schema: - type: object - description: OK - Roles updated - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Not Found - User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - 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 - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.GetEventStaffRow' - type: array - description: OK - Return users - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Get all staff users for an event - tags: - - Event - /events/{eventId}/teams: - get: - description: Gets all teams for a specific event. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the event - in: path - name: event_id - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/services.TeamWithMembers' - type: array - description: Teams successfully retrieved. - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get an event's teams - tags: - - Team - post: - description: Creates a new team for a specific event and assigns the creator - as the owner. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the event - in: path - name: event_id - required: true - schema: - type: integer - requestBody: - content: - application/json: - schema: - oneOf: - - type: object - - $ref: '#/components/schemas/handlers.CreateTeamRequest' - description: Team Creation Payload - summary: request - description: Team Creation Payload - required: true - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/sqlc.Team' - description: A team object - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad request: you had request parameters needed for this method.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Conflict: You already have a team.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Create a new team - tags: - - Team - /events/{eventId}/teams/{teamId}/join: - post: - description: Requests to join a team or fails if user is already on a team. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - - description: The ID of the event - in: path - name: event_id - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.CreateJoinRequest' - description: Team Creation Payload - summary: request - description: Team Creation Payload - required: true - responses: - "204": - description: Successfully left the team - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Conflict: User is already on a team.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Request to join a team - tags: - - Team - /events/{eventId}/teams/me: - get: - description: Retrieves the team information and the full list of team members - for the currently authenticated user within a specified event. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the event - in: path - name: event_id - required: true - schema: - type: integer - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/services.TeamWithMembers' - description: Team information and members successfully retrieved. - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Team not found for the user in this event. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get the authenticated user's team and its members for this specific - event. - tags: - - Team - /events/{eventId}/teams/me/pending-joins: - get: - description: Retrieves the current user's pending requests for a specific event's - teams. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.TeamJoinRequest' - type: array - description: Successfully retrieved pending requests - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get your pending requests - tags: - - Team - /events/{eventId}/users: - get: - description: Gets all users with any role for the event - parameters: - - description: Event ID - in: path - name: eventId - required: true - schema: - format: uuid - type: string - requestBody: - content: - application/json: - schema: - type: object - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.GetEventStaffRow' - type: array - description: OK - Return users - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: Something went terribly wrong on our end.' - summary: Get all users for an event - 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: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Bad request/Malformed request. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Server Error: error getting user info for event' - 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 - by a team id. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/services.TeamWithMembers' - description: Team information and members successfully retrieved. - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Team not found for the user in this event. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get a team and its members by team id. - tags: - - Team - /teams/{teamId}/members/{userId}: - delete: - description: Kicks a member from a team. Only the team owner can perform this - action. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - - description: The ID of the user to be kicked - in: path - name: userId - required: true - schema: - type: string - responses: - "204": - description: Successfully kicked the team member - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Forbidden: Requester is not allowed to perform this action.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went wrong. - summary: Kick a member from a team - tags: - - Team - /teams/{teamId}/members/me: - delete: - description: Leaves a team if the requester is on the team. Depends on cookies - for user retrieval. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - responses: - "204": - description: Successfully left the team - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Leave a team - tags: - - Team - /teams/{teamId}/pending-joins: - get: - description: Retrieves a team's pending join requests. This is only allowed - for the team's owner. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.ListJoinRequestsByTeamAndStatusWithUserRow' - type: array - description: Successfully retrieved pending requests - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Forbidden: Requester is not allowed to perform this action.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get team's pending join requests - tags: - - Team - /teams/join/{requestId}/accept: - post: - description: Accepts a pending team join request. Only the team owner can perform - this action. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - - description: The ID of the join request - in: path - name: request_id - required: true - schema: - type: string - responses: - "204": - description: Successfully accepted the join request - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Forbidden: Requester is not allowed to perform this action.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Not Found: The join request does not exist.' - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Conflict: The join request has already been responded to.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went wrong. - summary: Accept a team join request - tags: - - Team - /teams/join/{requestId}/reject: - post: - description: Rejects a pending team join request. Only the team owner can perform - this action. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session_id - required: true - schema: - type: string - - description: The ID of the team - in: path - name: team_id - required: true - schema: - type: string - - description: The ID of the join request - in: path - name: request_id - required: true - schema: - type: string - responses: - "204": - description: Successfully accepted the join request - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Bad Request: Missing or malformed parameters.' - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "403": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Forbidden: Requester is not allowed to perform this action.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Not Found: The join request does not exist.' - "409": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Conflict: The join request has already been responded to.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went wrong. - summary: Reject a team join request - tags: - - Team - /users: - get: - description: Get or search for users by name or email. If no search term is - provided, returns all users with pagination. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - - description: Search term to filter users by name or email (optional) - in: query - name: search - schema: - type: string - - description: Maximum number of users to return (default is 50) - in: query - name: limit - schema: - maximum: 100 - minimum: 1 - type: integer - - description: Number of users to skip for pagination (default is 0) - in: query - name: offset - schema: - minimum: 0 - type: integer - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/sqlc.AuthUser' - type: array - description: 'OK: Returns a list of users matching the search criteria, - or all users if no search term is provided.' - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Invalid query parameter(s) - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Failed to retrieve users - summary: Get/Search for users - tags: - - User - /users/email-consent: - patch: - description: Update the user's email consent setting - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.UpdateEmailConsentRequest' - description: The update email consent request body - summary: request - description: The update email consent request body - required: true - responses: - "200": - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Invalid request body - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Failed to update email consent - summary: Update Email Consent - tags: - - User - /users/me: - get: - description: Get profile information of the currently authenticated user. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/sqlc.AuthUser' - description: OK - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: User profile not found. - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Something went seriously wrong. - summary: Get User Profile - tags: - - User - patch: - description: Update the user's information - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.UpdateProfileRequest' - description: The update profile request body - summary: request - description: The update profile request body - required: true - responses: - "200": - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Invalid request body - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Failed to update user profile - summary: Update User - tags: - - User - /users/me/onboarding: - patch: - description: Onboard the user. - parameters: - - description: The authenticated session token/id - in: cookie - name: sh_session - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/handlers.CompleteOnboardingRequest' - description: The onboarding request body - summary: request - description: The onboarding request body - required: true - responses: - "200": - description: OK - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Invalid request body - "401": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: 'Unauthenticated: Requester is not currently authenticated.' - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: User not found - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/response.ErrorResponse' - description: Failed to complete onboarding - summary: Complete Onboarding - tags: - - User diff --git a/apps/api/download-openapi.sh b/apps/api/download-openapi.sh new file mode 100755 index 00000000..d1817490 --- /dev/null +++ b/apps/api/download-openapi.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +URL="http://localhost:8080/openapi.json" +DIR="docs" +OUTPUT="./$DIR/openapi.json" + +mkdir -p "$DIR" + +curl -s -o "$OUTPUT" "$URL" + +if [ $? -eq 0 ]; then + echo "Downloaded successfully: $OUTPUT" +else + echo "Failed to download from $URL" >&2 + exit 1 +fi \ No newline at end of file diff --git a/apps/api/go.mod b/apps/api/go.mod index de09374e..a9a365d7 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -1,78 +1,60 @@ module github.com/swamphacks/core/apps/api -go 1.24.0 - -toolchain go1.24.4 +go 1.26.1 require ( - github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 - github.com/aws/aws-sdk-go-v2 v1.39.2 - github.com/aws/aws-sdk-go-v2/config v1.31.1 - github.com/aws/aws-sdk-go-v2/credentials v1.18.5 - github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0 - github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 - github.com/caarlos0/env/v11 v11.3.1 - github.com/go-chi/chi/v5 v5.2.2 - github.com/go-chi/cors v1.2.1 - github.com/go-playground/validator/v10 v10.27.0 + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 + github.com/aws/aws-sdk-go-v2/service/ses v1.34.21 + github.com/caarlos0/env/v11 v11.4.0 + github.com/danielgtaylor/huma/v2 v2.37.2 + github.com/go-chi/chi/v5 v5.2.5 + github.com/go-chi/cors v1.2.2 + github.com/go-playground/validator/v10 v10.30.1 github.com/google/uuid v1.6.0 - github.com/hibiken/asynq v0.25.1 - github.com/jackc/pgx/v5 v5.7.4 + github.com/hibiken/asynq v0.26.0 + github.com/jackc/pgx/v5 v5.9.1 github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.34.0 - github.com/swaggo/swag/v2 v2.0.0-rc4.0.20250911100114-88e58922bf36 - golang.org/x/sync v0.17.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + golang.org/x/sync v0.19.0 ) require ( - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.28.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.37.1 // indirect - github.com/aws/smithy-go v1.23.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/spec v0.22.0 // indirect - github.com/go-openapi/swag/conv v0.25.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect - github.com/go-openapi/swag/jsonutils v0.25.1 // indirect - github.com/go-openapi/swag/loading v0.25.1 // indirect - github.com/go-openapi/swag/stringutils v0.25.1 // indirect - github.com/go-openapi/swag/typeutils v0.25.1 // indirect - github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/redis/go-redis/v9 v9.14.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/sv-tools/openapi v0.4.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.8.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/spf13/cast v1.10.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/apps/api/go.sum b/apps/api/go.sum index 60638828..b2dd34c6 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -1,54 +1,54 @@ -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 h1:W4Yar1SUsPmmA51qoIRb174uDO/Xt3C48MB1YX9Y3vM= -github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06/go.mod h1:/wotfjM8I3m8NuIHPz3S8k+CCYH80EqDT8ZeNLqMQm0= -github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= -github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= -github.com/aws/aws-sdk-go-v2/config v1.31.1 h1:PSQn4ObaQLaHl6qjs+XYH2pkxyHzZlk1GgQDrKlRJ7I= -github.com/aws/aws-sdk-go-v2/config v1.31.1/go.mod h1:3UA8Gj+2nzpV8WBUF0b19onBfz0YMXDQyGEW0Ru1ntI= -github.com/aws/aws-sdk-go-v2/credentials v1.18.5 h1:DATc1xnpHUV8VgvtnVQul+zuCwK6vz7gtkbKEUZcuNI= -github.com/aws/aws-sdk-go-v2/credentials v1.18.5/go.mod h1:y7aigZzjm1jUZuCgOrlBng+VJrKkknY2Cl0JWxG7vHU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3 h1:ZV2XK2L3HBq9sCKQiQ/MdhZJppH/rH0vddEAamsHUIs= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3/go.mod h1:b9F9tk2HdHpbf3xbN7rUZcfmJI26N6NcJu/8OsBFI/0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3 h1:3ZKmesYBaFX33czDl6mbrcHb6jeheg6LqjJhQdefhsY= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3/go.mod h1:7ryVb78GLCnjq7cw45N6oUb9REl7/vNUwjvIqC5UgdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3 h1:SE/e52dq9a05RuxzLcjT+S5ZpQobj3ie3UTaSf2NnZc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3/go.mod h1:zkpvBTsR020VVr8TOrwK2TrUW9pOir28sH5ECHpnAfo= -github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0 h1:egoDf+Geuuntmw79Mz6mk9gGmELCPzg5PFEABOHB+6Y= -github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0/go.mod h1:t9MDi29H+HDbkolTSQtbI0HP9DemAWQzUjmWC7LGMnE= -github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 h1:NwOeuOFrWoh4xWKINrmaAK4Vh75jmmY0RAuNjQ6W5Es= -github.com/aws/aws-sdk-go-v2/service/ses v1.34.5/go.mod h1:m3BsMJZD0eqjGIniBzwrNUqG9ZUPquC4hY9FyE2qNFo= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.1 h1:YfsU8hHGvVT+c6Q8MUs8haDbFQajAImrB7yZ9XnPcBY= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.1/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.1 h1:b4REsk5C0hooowAPmV8fS2haHb+HCyb5FKSKOZRBBfU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.1/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.1 h1:ssCHKyNJqTnqRH4Vlf+jI0brtGQYBvzWwnATsOMk1mk= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.1/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.21 h1:mXFOYIoae5eigd1rgoikbnWkFi5lOLp5EhIJu4hCVqI= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.21/go.mod h1:qCRiBxitqDG+NGbKdgbvllTUFEV9PUhlPnDtgY6tkBE= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= -github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc= +github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/danielgtaylor/huma/v2 v2.37.2 h1:Nf9vjy2sxBJFaupPlthXL/Hy2+LurfVbaKHmCMEI7xE= +github.com/danielgtaylor/huma/v2 v2.37.2/go.mod h1:95S04G/lExFRYlBkKaBaZm9lVmxRmqX9f2CgoOZ11AM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,56 +56,35 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= -github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= -github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= -github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= -github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= -github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= -github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= -github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= -github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= -github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= -github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw= -github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg= +github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw= +github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -116,64 +95,54 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.14.1 h1:nDCrEiJmfOWhD76xlaw+HXT0c9hfNWeXgl0vIRYSDvQ= +github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4= -github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE= -github.com/swaggo/swag/v2 v2.0.0-rc4.0.20250911100114-88e58922bf36 h1:cfbkf6v4Nfl9O+3gatpnrlFaX52Qmvqv0HGTn8hElgY= -github.com/swaggo/swag/v2 v2.0.0-rc4.0.20250911100114-88e58922bf36/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/api/internal/README.md b/apps/api/internal/README.md deleted file mode 100644 index d871cd7b..00000000 --- a/apps/api/internal/README.md +++ /dev/null @@ -1,5 +0,0 @@ -### Internal - -This is where most of our files/folders will _go_ (get it?). Anyways, handlers + services + more! - -This read me was made so this folder would be committed! diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index bc3ba63a..ac01b6b6 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -1,58 +1,75 @@ package api import ( - "fmt" + "context" "net/http" + "time" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" - "github.com/rs/zerolog" + "github.com/hibiken/asynq" "github.com/rs/zerolog/log" - - "github.com/swamphacks/core/apps/api/internal/api/handlers" mw "github.com/swamphacks/core/apps/api/internal/api/middleware" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - - "github.com/MarceloPetrucio/go-scalar-api-reference" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/domains/application" + "github.com/swamphacks/core/apps/api/internal/domains/auth" + "github.com/swamphacks/core/apps/api/internal/domains/bat" + "github.com/swamphacks/core/apps/api/internal/domains/email" + "github.com/swamphacks/core/apps/api/internal/domains/hackathon" + "github.com/swamphacks/core/apps/api/internal/domains/redeemables" + "github.com/swamphacks/core/apps/api/internal/domains/teams" + "github.com/swamphacks/core/apps/api/internal/domains/users" + "github.com/swamphacks/core/apps/api/internal/emailutils" + "github.com/swamphacks/core/apps/api/internal/logger" + "github.com/swamphacks/core/apps/api/internal/storage" ) -type API struct { - Router *chi.Mux - Logger *zerolog.Logger - Handlers *handlers.Handlers - Middleware *mw.Middleware -} +func Run() { + logger := logger.New() + config := config.LoadConfig() + + db := database.NewDB(config.DatabaseURL) + defer db.Close() -func NewAPI(logger *zerolog.Logger, handlers *handlers.Handlers, middleware *mw.Middleware) *API { - api := &API{ - Router: chi.NewRouter(), - Logger: logger, - Handlers: handlers, - Middleware: middleware, + txm := database.NewTransactionManager(db) + + httpClient := &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, } - api.setupRoutes(api.Middleware) + // Create SES Client for email service + sesClient := emailutils.NewSESClient(config.AWS.AccessKey, config.AWS.AccessKeySecret, config.AWS.Region, logger) - return api -} + // Create asynq client + redisOpt, err := asynq.ParseRedisURI(config.RedisURL) + if err != nil { + logger.Fatal().Msg("Failed to parse REDIS_URL") + } + taskQueueClient := asynq.NewClient(redisOpt) -func (api *API) setupRoutes(mw *mw.Middleware) { - var ( - // Both requireXXRole functions automatically allow superusers - ensureSuperuser = mw.Auth.RequirePlatformRole([]sqlc.AuthUserRole{sqlc.AuthUserRoleSuperuser}) - ensureEventAdmin = mw.Event.RequireEventRole([]sqlc.EventRoleType{sqlc.EventRoleTypeAdmin}) - // Event Admins are technically Staff... - ensureEventStaff = mw.Event.RequireEventRole([]sqlc.EventRoleType{sqlc.EventRoleTypeAdmin, sqlc.EventRoleTypeStaff}) - ) + r2Client, err := storage.NewR2Client(config.CF.AccountID, config.CF.AccessKeyId, config.CF.AccessKeySecret, logger) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to create R2 client") + } - AllowedOrigins := config.Load().AllowedOrigins + r := chi.NewRouter() - api.Router.Use(middleware.Logger) - api.Router.Use(middleware.RealIP) - api.Router.Use(cors.Handler(cors.Options{ - AllowedOrigins: AllowedOrigins, + r.Use(middleware.Logger) + r.Use(middleware.RealIP) + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: config.AllowedOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, @@ -60,236 +77,90 @@ func (api *API) setupRoutes(mw *mw.Middleware) { MaxAge: 300, })) - api.Router.Get("/docs", func(w http.ResponseWriter, r *http.Request) { - htmlContent, err := scalar.ApiReferenceHTML(&scalar.Options{ - // SpecURL: "https://generator3.swagger.io/openapi.json",// allow external URL or local path file - SpecURL: "./docs/swagger.json", - CustomOptions: scalar.CustomOptions{ - PageTitle: "SwampHacks API", - }, - DarkMode: true, - }) - - if err != nil { - fmt.Printf("%v", err) - } - - fmt.Fprintln(w, htmlContent) - }) - - api.Router.Route("/mobile", func(r chi.Router) { - // This means you have to set a Authorization header with "Key xxx". - r.Use(mw.Auth.RequireMobileAuth) - - r.Get("/events/{eventId}/users/{userId}", api.Handlers.Event.GetUserForEvent) - r.Get("/events/{eventId}/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID) - r.Post("/events/{eventId}/checkin", api.Handlers.Admission.HandleEventCheckIn) - - r.Get("/events/{eventId}/redeemables", api.Handlers.Redeemables.GetRedeemables) - r.Post("/redeemables/{redeemableId}/users/{userId}", api.Handlers.Redeemables.RedeemRedeemable) - r.Post("/events/{eventId}/users/{userId}/update-rfid", api.Handlers.Event.UpdateUserRFID) - }) - - // Health check - api.Router.Get("/ping", func(w http.ResponseWriter, r *http.Request) { - api.Logger.Trace().Str("method", r.Method).Str("path", r.URL.Path).Msg("Received ping.") - w.Header().Set("Content-Type", "text/plain") - w.Header().Set("Content-Length", "6") - if _, err := w.Write([]byte("pong!\n")); err != nil { - log.Err(err) - } - }) - - // --- Auth routes --- - api.Router.Route("/auth", func(r chi.Router) { - r.Get("/callback", api.Handlers.Auth.OAuthCallback) - - // Protected auth routes - r.Group(func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - r.Get("/me", api.Handlers.Auth.GetMe) - r.Post("/logout", api.Handlers.Auth.Logout) - }) - }) - - // --- User routes --- - api.Router.Route("/users", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - r.Get("/", api.Handlers.User.GetUsers) - r.Get("/me", api.Handlers.User.GetProfile) - r.Patch("/me", api.Handlers.User.UpdateUser) - r.Patch("/me/email-consent", api.Handlers.User.UpdateEmailConsent) - r.Patch("/me/onboarding", api.Handlers.User.CompleteOnboarding) - }) - - // --- Team routes (non Event specific) --- - api.Router.Route("/teams", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - r.Get("/{teamId}", api.Handlers.Teams.GetTeam) - r.Get("/{teamId}/pending-joins", api.Handlers.Teams.GetPendingRequestsForTeam) - r.Delete("/{teamId}/members/me", api.Handlers.Teams.LeaveTeam) - r.Delete("/{teamId}/members/{userId}", api.Handlers.Teams.KickMemberFromTeam) - r.Post("/join/{requestId}/accept", api.Handlers.Teams.AcceptTeamJoinRequest) - r.Post("/join/{requestId}/reject", api.Handlers.Teams.RejectTeamJoinRequest) - }) - - // --- Discord routes (for Discord bot) --- - api.Router.Route("/discord", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - r.Get("/event/{event_id}/attendees", api.Handlers.Discord.GetEventAttendeesWithDiscord) - }) - - // --- Event routes --- - api.Router.Route("/events", func(r chi.Router) { - // Superuser-only - r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent) - - // Authenticated - r.With(mw.Auth.RequireAuth).Get("/", api.Handlers.Event.GetEvents) - - r.Post("/{eventId}/interest", api.Handlers.EventInterest.AddEmailToEvent) // Unprotected - - // Event-specific routes - r.Route("/{eventId}", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) // routes below this are protected - - r.Get("/", api.Handlers.Event.GetEventByID) - r.Get("/role", api.Handlers.Event.GetEventRole) - r.Get("/discord/{discordId}", api.Handlers.Discord.GetUserEventRoleByDiscordIDAndEventId) - - r.With(ensureEventStaff).Get("/overview", api.Handlers.Event.GetEventOverview) - - // Check in and event day of routes (STAFF+) - r.With(ensureEventStaff).Post("/checkin", api.Handlers.Admission.HandleEventCheckIn) - // Used to fetch user info for checking in - r.With(ensureEventStaff).Get("/users/{userId}", api.Handlers.Event.GetUserForEvent) - // Get user ID by RFID - r.With(ensureEventStaff).Get("/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID) - // Is the user checked in - r.With(ensureEventStaff).Get("/users/{userId}/checked-in-status", api.Handlers.Event.GetCheckedInStatusByIds) - - // Admin-only - r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail) - r.With(ensureEventAdmin).Post("/queue-welcome-email", api.Handlers.Email.QueueWelcomeEmail) - r.With(ensureEventAdmin).Post("/send-welcome-emails", api.Handlers.Bat.SendWelcomeEmails) - r.With(ensureEventAdmin).Post("/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest) - r.With(ensureEventAdmin).Patch("/transition-waitlisted-applications", api.Handlers.Application.TransitionWaitlistedApplications) - r.With(ensureEventAdmin).Post("/begin-waitlist-transition", api.Handlers.Bat.QueueScheduleWaitlistTransitionTask) - r.With(ensureEventAdmin).Post("/shutdown-waitlist-scheduler", api.Handlers.Bat.QueueShutdownWaitlistSchedulerTask) - r.With(ensureEventAdmin).Post("/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions) - r.With(ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById) - r.With(ensureEventAdmin).Post("/banner", api.Handlers.Event.UploadEventBanner) - r.With(ensureEventAdmin).Delete("/banner", api.Handlers.Event.DeleteBanner) - r.With(ensureEventAdmin).Get("/staff", api.Handlers.Event.GetEventStaffUsers) - r.With(ensureEventAdmin).Get("/users", api.Handlers.Event.GetEventUsers) - r.With(ensureEventAdmin).Post("/roles", api.Handlers.Event.AssignEventRole) - r.With(ensureEventAdmin).Delete("/roles/{userId}", api.Handlers.Event.RevokeEventRole) - r.With(ensureEventAdmin).Post("/roles/batch", api.Handlers.Event.BatchAssignEventRoles) - r.With(ensureEventAdmin).Get("/bat-runs", api.Handlers.Bat.GetRunsByEventId) - r.With(ensureEventAdmin).Delete("/bat-runs", api.Handlers.Bat.GetRunsByEventId) - r.With(ensureEventAdmin).Get("/review-status", api.Handlers.Bat.CheckApplicationReviewsComplete) - r.With(ensureEventAdmin).Post("/reviews/bat-runs", api.Handlers.Admission.HandleCalculateAdmissionsRequest) - r.With(ensureEventAdmin).Post("/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions) - - // Superuser-only - r.With(ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById) - - // Application routes - r.Route("/application", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - r.Get("/", api.Handlers.Application.GetMyApplication) - r.Post("/submit", api.Handlers.Application.SubmitApplication) - r.Post("/save", api.Handlers.Application.SaveApplication) - r.Get("/download-resume", api.Handlers.Application.DownloadResume) - r.With(mw.Event.AttachEventRoleToContext()).Get("/{applicationId}/resume", api.Handlers.Application.GetResumePresignedUrl) - - // Getting a resume (Staff Only) - r.With(ensureEventStaff).Get("/{applicationId}", api.Handlers.Application.GetApplication) - - // For statistics (Staff ONLY) - r.With(ensureEventStaff).Get("/stats", api.Handlers.Application.GetApplicationStatistics) - - // For application review (Staff ONLY) - r.With(ensureEventStaff).Get("/assigned", api.Handlers.Application.GetAssignedApplications) - r.With(ensureEventStaff).Post("/{applicationId}/review", api.Handlers.Application.SubmitApplicationReview) - - // Review admin routes (For Event Admins only) - r.With(ensureEventAdmin).Post("/reset-reviews", api.Handlers.Application.ResetApplicationReviews) - r.With(ensureEventAdmin).Post("/assign-reviewers", api.Handlers.Application.AssignApplicationReviewers) - - //withdraw Acceptance - r.Patch("/withdraw-acceptance", api.Handlers.Application.WithdrawAcceptance) - - //Accept acceptance - r.Patch("/accept-acceptance", api.Handlers.Application.AcceptApplicationAcceptance) - - //Withdraw attendance - r.Patch("/withdraw-attendance", api.Handlers.Application.WithdrawAttendance) - - //Waitlist application - r.Patch("/join-waitlist", api.Handlers.Application.JoinWaitlist) - }) - - r.Route("/redeemables", func(r chi.Router) { - r.Use(ensureEventStaff) - // Get all redeemables and create new redeemable - // eventId is available from parent route context - r.Get("/", api.Handlers.Redeemables.GetRedeemables) - r.Post("/", api.Handlers.Redeemables.CreateRedeemable) - - // Update and delete specific redeemable - r.Route("/{redeemableId}", func(r chi.Router) { - r.Patch("/", api.Handlers.Redeemables.UpdateRedeemable) - r.With(ensureEventAdmin).Delete("/", api.Handlers.Redeemables.DeleteRedeemable) - - r.Route("/users/{userId}", func(r chi.Router) { - r.Post("/", api.Handlers.Redeemables.RedeemRedeemable) - r.Patch("/", api.Handlers.Redeemables.UpdateRedemption) - }) - }) - }) - - // Team routes - r.Route("/teams", func(r chi.Router) { - r.Post("/", api.Handlers.Teams.CreateTeam) - r.Get("/", api.Handlers.Teams.GetEventTeams) - r.Get("/me", api.Handlers.Teams.GetMyTeam) - r.Get("/me/pending-joins", api.Handlers.Teams.GetMyPendingRequests) - - // Specific team routes within events - r.Route("/{teamId}", func(r chi.Router) { - r.Post("/join", api.Handlers.Teams.RequestToJoinTeam) - }) - }) - }) + humaConfig := huma.DefaultConfig("SwampHacks API", "1.0.0") + humaConfig.DocsRenderer = huma.DocsRendererScalar + humaConfig.CreateHooks = nil + + // TODO: figure out a way to override the default schema name + // humaConfig.OpenAPI.Components = &huma.Components{ + // Schemas: huma.NewMapRegistry("#/components/schemas/", func(t reflect.Type, hint string) string { + // if t.Kind() == reflect.Pointer { + // t = t.Elem() + // } + + // bodyField, ok := t.FieldByName("Body") + + // if !ok { + // return huma.DefaultSchemaNamer(t, hint) + // } + + // if schemaName := bodyField.Tag.Get("schemaName"); schemaName != "" { + // return schemaName + // } + + // return huma.DefaultSchemaNamer(t, hint) + // }), + // } + + api := humachi.New(r, humaConfig) + + // Repositories Setup + userRepo := repository.NewUserRepository(db) + accountRepo := repository.NewAccountRespository(db) + sessionRepo := repository.NewSessionRepository(db) + hackathonRepo := repository.NewHackathonRepository(db) + applicationRepo := repository.NewApplicationRepository(db) + teamRepo := repository.NewTeamRespository(db) + teamMemberRepo := repository.NewTeamMemberRespository(db) + teamJoinRequestRepo := repository.NewTeamJoinRequestRepository(db) + redeemablesRepo := repository.NewRedeemablesRepository(db) + batRunsRepo := repository.NewBatRunsRepository(db) + eventInterestsRepo := repository.NewEventInterestsRepository(db) + + mw := mw.NewMiddleware(userRepo, db, logger, config) + + // Routes registrations + authService := auth.NewService(userRepo, accountRepo, sessionRepo, txm, httpClient, logger, &config.Auth) + authHandler := auth.NewHandler(authService, config, logger) + auth.RegisterRoutes(authHandler, huma.NewGroup(api, "/auth"), mw, config) + + userService := users.NewService(userRepo, logger) + userHandler := users.NewHandler(userService, config, logger) + users.RegisterRoutes(userHandler, huma.NewGroup(api, "/users"), mw) + + hackathonService := hackathon.NewService(hackathonRepo, userRepo, eventInterestsRepo, r2Client, &config.CoreBuckets, logger) + hackathonHandler := hackathon.NewHandler(hackathonService, config, logger) + hackathon.RegisterRoutes(hackathonHandler, huma.NewGroup(api, "/hackathon"), mw) + + emailService := email.NewEmailService(hackathonRepo, userRepo, taskQueueClient, sesClient, r2Client, logger, config) + batService := bat.NewBatService(applicationRepo, hackathonRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, config, logger) + applicationService := application.NewService(applicationRepo, userRepo, hackathonRepo, txm, r2Client, &config.CoreBuckets, nil, emailService, batService, config, logger) + applicationHandler := application.NewHandler(applicationService, batService, config, logger) + application.RegisterRoutes(applicationHandler, huma.NewGroup(api, "/application"), mw) + + teamService := teams.NewService(teamRepo, teamMemberRepo, teamJoinRequestRepo, hackathonRepo, userRepo, txm, logger) + teamHandler := teams.NewHandler(teamService, logger) + teams.RegisterRoutes(teamHandler, huma.NewGroup(api, "/teams"), mw) + + redeemablesService := redeemables.NewService(redeemablesRepo, logger) + redeemablesHandler := redeemables.NewHandler(redeemablesService, config, logger) + redeemables.RegisterRoutes(redeemablesHandler, huma.NewGroup(api, "/redeemables"), mw) + + huma.Register(api, huma.Operation{ + OperationID: "ping", + Method: http.MethodGet, + Summary: "Ping", + Description: "Health Check", + Tags: []string{"Misc"}, + Path: "/ping", + }, func(ctx context.Context, input *struct{}) (*struct{ Body string }, error) { + return &struct{ Body string }{ + Body: "pong", + }, nil }) - // Protected test routes - api.Router.Route("/protected", func(r chi.Router) { - r.Use(mw.Auth.RequireAuth) - - r.Get("/basic", func(w http.ResponseWriter, r *http.Request) { - if _, err := w.Write([]byte("Welcome, arbitrarily roled user!\n")); err != nil { - log.Err(err) - } - }) - - r.Group(func(r chi.Router) { - r.Use(mw.Auth.RequirePlatformRole([]sqlc.AuthUserRole{sqlc.AuthUserRoleUser})) - r.Get("/user", func(w http.ResponseWriter, r *http.Request) { - if _, err := w.Write([]byte("Welcome, user!\n")); err != nil { - log.Err(err) - } - }) - }) - - r.Group(func(r chi.Router) { - r.Use(mw.Auth.RequirePlatformRole([]sqlc.AuthUserRole{sqlc.AuthUserRoleSuperuser})) - r.Get("/superuser", func(w http.ResponseWriter, r *http.Request) { - if _, err := w.Write([]byte("Welcome, superuser!\n")); err != nil { - log.Err(err) - } - }) - }) - }) + logger.Info().Msgf("API listening on port %s", config.Port) + if err := http.ListenAndServe(":"+config.Port, r); err != nil { + log.Fatal().Msg("Failed to start server.") + } } diff --git a/apps/api/internal/api/cookie/cookie.go b/apps/api/internal/api/cookie/cookie.go new file mode 100644 index 00000000..528d34ce --- /dev/null +++ b/apps/api/internal/api/cookie/cookie.go @@ -0,0 +1,33 @@ +package cookie + +import ( + "net/http" + "time" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/swamphacks/core/apps/api/internal/config" +) + +var SessionCookieName = "sh_session_id" + +var SessionCookieHumaParam *huma.Param = &huma.Param{ + Name: SessionCookieName, + In: "cookie", + Required: true, + Schema: &huma.Schema{Type: "string"}, + Description: "Session cookie used to authenticate the user", +} + +func SetSessionCookie(w http.ResponseWriter, sessionID uuid.UUID, expiresAt time.Time, cfg config.CookieConfig) { + http.SetCookie(w, &http.Cookie{ + Name: SessionCookieName, + Value: sessionID.String(), + Domain: cfg.Domain, + Path: "/", + HttpOnly: true, + Secure: cfg.Secure, + SameSite: http.SameSiteLaxMode, + Expires: expiresAt, + }) +} diff --git a/apps/api/internal/api/handlers/admissions.go b/apps/api/internal/api/handlers/admissions.go deleted file mode 100644 index 48650f23..00000000 --- a/apps/api/internal/api/handlers/admissions.go +++ /dev/null @@ -1,145 +0,0 @@ -package handlers - -import ( - "encoding/json" - "errors" - "net/http" - - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type AdmissionHandler struct { - batService *services.BatService - logger zerolog.Logger -} - -func NewAdmissionHandler(batService *services.BatService, logger zerolog.Logger) *AdmissionHandler { - return &AdmissionHandler{ - batService: batService, - logger: logger.With().Str("handler", "AdmissionHandler").Logger(), - } -} - -func (h *AdmissionHandler) ReleaseDecisions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing event_id."), - ) - return - } - - runId, err := web.PathParamToUUID(r, "runId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing run_id."), - ) - return - } - - err = h.batService.ReleaseBatRunDecision(ctx, eventId, runId) - if err == nil { - w.WriteHeader(http.StatusNoContent) - return - } - - switch { - case errors.Is(err, services.ErrRunMismatch): - res.SendError(w, http.StatusForbidden, - res.NewError("run_mismatch", err.Error()), - ) - - case errors.Is(err, services.ErrRunStatusInvalid): - res.SendError(w, http.StatusConflict, - res.NewError("invalid_run_status", err.Error()), - ) - - case errors.Is(err, services.ErrNoAcceptedApplicants): - res.SendError(w, http.StatusUnprocessableEntity, - res.NewError("no_accepted_applicants", err.Error()), - ) - - case errors.Is(err, services.ErrCouldNotGetEventInfo): - res.SendError(w, http.StatusNotFound, - res.NewError("resource_not_found", err.Error()), - ) - - case errors.Is(err, services.ErrFailedToUpdateRun): - res.SendError(w, http.StatusInternalServerError, - res.NewError("update_failed", "Failed to release decisions."), - ) - - default: - res.SendError(w, http.StatusInternalServerError, - res.NewError("internal_error", "An unexpected error occurred."), - ) - } -} - -func (h *AdmissionHandler) HandleCalculateAdmissionsRequest(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) - return - } - - _, err = h.batService.QueueCalculateAdmissionsTask(r.Context(), eventId) - if err != nil { - res.Send(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went terribly wrong.")) - return - } - - w.WriteHeader(http.StatusCreated) - -} - -type EventCheckInRequest struct { - UserID uuid.UUID `json:"user_id"` - RFID *string `json:"rfid"` -} - -// Checks a user into an event. Staff only role. -// -// @Summary Check a user into an event -// @Description Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet. -// @Tags Admissions -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param event_id path int true "The ID of the event" -// @Param request body EventCheckInRequest true "Event check in data" -// @Success 204 "No Content" -// @Failure 400 {object} response.ErrorResponse "Malformed request body." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/checkin [post] -func (h *AdmissionHandler) HandleEventCheckIn(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_param", "Missing event id")) - return - } - - var req EventCheckInRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - - if *req.RFID == "" { - req.RFID = nil - } - - err = h.batService.CheckInAttendee(r.Context(), eventId, req.UserID, req.RFID) - if err != nil { - res.Send(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went terribly wrong.")) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/apps/api/internal/api/handlers/application.go b/apps/api/internal/api/handlers/application.go deleted file mode 100644 index 308c53ca..00000000 --- a/apps/api/internal/api/handlers/application.go +++ /dev/null @@ -1,731 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "errors" - "io" - "net/http" - "strconv" - - "github.com/go-chi/chi/v5" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type ApplicationHandler struct { - appService *services.ApplicationService -} - -func NewApplicationHandler(appService *services.ApplicationService) *ApplicationHandler { - return &ApplicationHandler{ - appService: appService, - } -} - -// Get current user's application by event ID -// -// @Summary Get Current User's Application by Event ID -// @Description Get the current user's application progress for an event. If this is their first time filling out the application, a new application will be created. -// @Tags Application -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Success 200 {object} sqlc.Application "OK: An application was found" -// @Success 200 {object} map[string]any "OK: An application was found" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving application"\ -// @Router /events/{eventId}/application [get] -func (h *ApplicationHandler) GetMyApplication(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - params := sqlc.GetApplicationByUserAndEventIDParams{ - UserID: *userId, - EventID: eventId, - } - - application, err := h.appService.GetApplicationByUserAndEventID(r.Context(), params) - if err != nil { - if errors.Is(err, repository.ErrApplicationNotFound) { - params := sqlc.CreateApplicationParams{ - UserID: *userId, - EventID: eventId, - } - - newApplication, err := h.appService.CreateApplication(r.Context(), params) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("create_application_error", "can't create application")) - return - } - - if newApplication == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("create_application_error", "can't create application")) - return - } - - res.Send(w, http.StatusOK, newApplication) - return - } - if errors.Is(err, services.ErrApplicationUnavailable) { - res.SendError(w, http.StatusBadRequest, res.NewError("get_application_error", "the application is unavailable")) - return - } - - res.SendError(w, http.StatusBadRequest, res.NewError("get_application_error", "error retrieving application")) - return - } - - res.Send(w, http.StatusOK, application) -} - -// Submit Application -// -// @Summary Submit Application -// @Description Submit the application for an event. -// @Tags Application -// @Accept json -// @Produce json -// @Param formBody formData any true "Submission form data" -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error submitting application" -// @Router /events/{eventId}/application/submit [post] -func (h *ApplicationHandler) SubmitApplication(w http.ResponseWriter, r *http.Request) { - // Parse multipart form (10 MB max memory) - err := r.ParseMultipartForm(10 << 20) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("parse_form_invalid", "Failed to parse form: "+err.Error())) - return - } - - var submission services.ApplicationSubmissionFields - - // Map form values - submission.FirstName = r.FormValue("firstName") - submission.LastName = r.FormValue("lastName") - - if ageStr := r.FormValue("age"); ageStr != "" { - if age, err := strconv.Atoi(ageStr); err == nil { - submission.Age = age - } - } - - submission.Phone = r.FormValue("phone") - submission.PreferredEmail = r.FormValue("preferredEmail") - submission.UniversityEmail = r.FormValue("universityEmail") - - submission.Country = r.FormValue("country") - submission.Gender = r.FormValue("gender") - submission.GenderOther = r.FormValue("gender-other") - submission.Pronouns = r.FormValue("pronouns") - submission.Race = r.FormValue("race") - submission.RaceOther = r.FormValue("race-other") - submission.Orientation = r.FormValue("orientation") - - submission.Linkedin = r.FormValue("linkedin") - submission.Github = r.FormValue("github") - - if ageCertStr := r.FormValue("ageCertification"); ageCertStr != "" { - submission.AgeCertification = (ageCertStr == "true" || ageCertStr == "1") - } - - submission.School = r.FormValue("school") - submission.Level = r.FormValue("level") - submission.LevelOther = r.FormValue("level-other") - submission.Year = r.FormValue("year") - submission.YearOther = r.FormValue("year-other") - submission.GraduationYear = r.FormValue("graduationYear") - submission.Majors = r.FormValue("majors") - submission.Minors = r.FormValue("minors") - submission.Experience = r.FormValue("experience") - submission.UfHackathonExp = r.FormValue("ufHackathonExp") - submission.ProjectExperience = r.FormValue("projectExperience") - submission.ShirtSize = r.FormValue("shirtSize") - submission.Diet = r.FormValue("diet") - submission.Essay1 = r.FormValue("essay1") - submission.Essay2 = r.FormValue("essay2") - submission.Referral = r.FormValue("referral") - submission.PictureConsent = r.FormValue("pictureConsent") - submission.InPersonAcknowledgement = r.FormValue("inpersonAcknowledgement") - submission.AgreeToConduct = r.FormValue("agreeToConduct") - submission.InfoShareAuthorization = r.FormValue("infoShareAuthorization") - submission.AgreeToMLHEmails = r.FormValue("agreeToMLHEmails") - - resumeFile, _, err := r.FormFile("resume[]") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "invalid resume file")) - return - } - - defer resumeFile.Close() - - resumeFileBuffer := bytes.NewBuffer(nil) - - if _, err := io.Copy(resumeFileBuffer, resumeFile); err != nil { - return - } - - validate := validator.New() - if err := validate.Struct(submission); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) - return - } - - eventIdStr := chi.URLParam(r, "eventId") - - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - err = h.appService.SubmitApplication(r.Context(), submission, resumeFileBuffer.Bytes(), *userId, eventId) - - if err != nil { - if errors.Is(err, services.ErrApplicationDeadlinePassed) { - res.SendError(w, http.StatusInternalServerError, res.NewError("submit_application_error", services.ErrApplicationDeadlinePassed.Error())) - return - } - - res.SendError(w, http.StatusInternalServerError, res.NewError("submit_application_error", "Something went wrong while submitting application")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Save Application -// -// @Summary Save Application -// @Description Save user's progress on the application. File/Upload fields are not saved. -// @Tags Application -// @Accept json -// @Produce json -// @Param data body any true "Form data" -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error saving application" -// @Router /events/{eventId}/application/save [post] -func (h *ApplicationHandler) SaveApplication(w http.ResponseWriter, r *http.Request) { - var data any - - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_form_data", "Something went wrong while parsing form submission")) - return - } - - eventIdStr := chi.URLParam(r, "eventId") - - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - err = h.appService.SaveApplication(r.Context(), data, *userId, eventId) - - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("save_application_error", "Something went wrong while saving application")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Download Resume -// -// @Summary Download the user's uploaded resume from their event application -// @Description This handler creates a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object. -// @Tags Application -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 {object} string -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error handling download resume request" -// @Router /events/{eventId}/application/download-resume [get] -func (h *ApplicationHandler) DownloadResume(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - request, err := h.appService.DownloadResume(r.Context(), *userId, eventId, 60) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("resume_download_error", "unable to retrieve resume download url")) - return - } - - res.Send(w, http.StatusOK, request.URL) -} - -// Get Application Statistics -// -// @Summary Gets an event's submitted application statistics -// @Description This aggregates applications by race, gender, age, majors, and schools. This route is only available to event staff and admins. -// @Tags Application -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 {object} services.ApplicationStatistics -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error getting statistics" -// @Router /events/{eventId}/application/stats [get] -func (h *ApplicationHandler) GetApplicationStatistics(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID.")) - return - } - - appStats, err := h.appService.GetApplicationStatistics(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("application_stats_err", "Something went wrong while aggregating application statistics.")) - return - } - - res.Send(w, http.StatusOK, appStats) -} - -// Get an application for a user and event -// -// @Summary Get an application based on a user id and event id. -// @Description Retrieves an application using the user id and event id primary keys and unique constraints. Only accessible by event staff and admins. -// @Tags Application -// @Produce json -// @Param eventId path string true "Event ID" -// @Param applicationId path string true "Application ID (Technically user ID)" -// @Param sh_session cookie string true "The authenticated session token/id" -// @Success 200 {object} sqlc.Application "OK: An application was found" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving assigned application" -// @Router /events/{eventId}/application/{applicationId} [get] -func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - // So funny story, there is no ID in the application table, this is just an abstracted user_id. - applicationId, err := web.PathParamToUUID(r, "applicationId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not valid.")) - return - } - - application, err := h.appService.GetApplicationByUserAndEventID(r.Context(), sqlc.GetApplicationByUserAndEventIDParams{ - UserID: applicationId, - EventID: eventId, - }) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("get_assigned_application_error", "error retrieving assigned application")) - return - } - - res.Send(w, http.StatusOK, application) -} - -type ReviewRatings struct { - PassionRating int `json:"passion_rating" validate:"required,min=1,max=5"` - ExperienceRating int `json:"experience_rating" validate:"required,min=1,max=5"` -} - -// Submit application review -// -// @Summary Submit application review -// @Description Handles ratings submissions from staff during the application review process. -// @Tags Application -// @Produce json -// @Param reviewData body ReviewRatings true "An object containing the passion and experience ratings" -// @Success 201 -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error submitting application review" -// @Router /events/{eventId}/application/{applicationId}/review [post] -func (h *ApplicationHandler) SubmitApplicationReview(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - applicationId, err := web.PathParamToUUID(r, "applicationId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not valid.")) - return - } - - reviewerId := ctxutils.GetUserIdFromCtx(r.Context()) - if reviewerId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - var reviewData ReviewRatings - if err := json.NewDecoder(r.Body).Decode(&reviewData); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Failed to parse request body: "+err.Error())) - return - } - - validate := validator.New() - if err := validate.Struct(reviewData); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) - return - } - - if err = h.appService.SaveApplicationReview(r.Context(), *reviewerId, applicationId, eventId, reviewData.ExperienceRating, reviewData.PassionRating); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("save_review_error", "Something went wrong while saving the application review.")) - return - } - - w.WriteHeader(http.StatusCreated) -} - -// Get Assigned Application IDs and Progress -// -// @Summary Get Assigned Application IDs and Progress -// @Description Retrieves assigned applications and their review progress for the authenticated reviewer. -// @Tags Application -// @Produce json -// @Param eventId path string true "Event ID" -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Success 200 {array} services.AssignedApplication "OK: An application was found" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error retrieving assigned application" -// @Router /events/{eventId}/application/assigned [get] -func (h *ApplicationHandler) GetAssignedApplications(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "invalid user id")) - return - } - - assignedApps, err := h.appService.GetAssignedApplicationsAndProgress(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("get_assigned_applications_error", "Something went wrong while retrieving assigned applications.")) - return - } - - res.Send(w, http.StatusOK, assignedApps) -} - -// Assign application to reviewers -// -// @Summary Assign application to reviewers -// @Description Assigns applications for an event to reviewers for the application review process. -// @Tags Application -// @Accept json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param request body []services.ReviewerAssignment true "Reviewer assignmnet payload" -// @Success 201 "Reviewers assigned" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error assigning reviewers" -// @Router /events/{eventId}/application/assign-reviewers [post] -func (h *ApplicationHandler) AssignApplicationReviewers(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - var payload []services.ReviewerAssignment - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Failed to parse request body: "+err.Error())) - return - } - - // Process assignments - err = h.appService.AssignReviewers(r.Context(), eventId, payload) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("assign_reviewers_error", "Something went wrong while assigning reviewers to applications.")) - return - } - - w.WriteHeader(http.StatusCreated) -} - -// Reset application reviews -// -// @Summary Reset application reviews -// @Description Resets all application reviews for a given event, clearing any existing reviewer assignments. -// @Tags Application -// -// @Param eventId path string true "ID of the event to reset reviews for" -// @Success 200 "Application reviews reset successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to reset application reviews" -// @Router /events/{eventId}/application/reset-reviews [post] -func (h *ApplicationHandler) ResetApplicationReviews(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - err = h.appService.ResetApplicationReviews(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("reset_reviews_error", "Something went wrong while resetting application reviews.")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Get resume -// -// @Summary Get resume for application review -// @Description This handler creates a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review. -// @Tags Application -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param applicationId path string true "The application ID (userId of applicant)" Format(uuid) -// @Success 200 {object} string -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error handling download resume request" -// @Router /events/{eventId}/application/{applicationId}/resume [get] -func (h *ApplicationHandler) GetResumePresignedUrl(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - applicationId, err := web.PathParamToUUID(r, "applicationId") - - // Ensure access - userId := ctxutils.GetUserIdFromCtx(r.Context()) - eventRole := ctxutils.GetEventRoleFromCtx(r.Context()) - - if userId == nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "the user id of the current user is invalid")) - return - } - - if eventRole.Role != sqlc.EventRoleTypeStaff && eventRole.Role != sqlc.EventRoleTypeAdmin && *userId != applicationId { - res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are not allowed to see other ppls resumes :(")) - return - } - - request, err := h.appService.DownloadResume(r.Context(), applicationId, eventId, 600) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("resume_download_error", "unable to retrieve resume download url")) - return - } - - res.Send(w, http.StatusOK, request.URL) -} - -// Join Waitlist for an event -// -// @Summary Join event waitlist after rejected application status. -// @Description Adds a waitlist join time to application. Sets status to waitlisted -// @Tags Application -// -// @Param eventId path string true "ID of the event to join the waitlist for" -// @Success 200 "Event Waitlist joined successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to join waitlist" -// @Router /events/{eventId}/application/join-waitlist [patch] -func (h *ApplicationHandler) JoinWaitlist(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - err = h.appService.JoinWaitlist(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("join_waitlist_error", "Something went wrong while joining waitlist")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Withdraw an acceptance to an event -// -// @Summary Withdraw an acceptance after being accepted to an event. -// @Description Sets application status from accepted to rejected -// @Tags Application -// -// @Param eventId path string true "ID of the event to withdraw acceptance from" -// @Success 200 "Acceptance withdrawn successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to withdraw acceptance" -// @Router /events/{eventId}/application/withdraw-acceptance [patch] -func (h *ApplicationHandler) WithdrawAcceptance(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - err = h.appService.WithdrawAcceptance(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("withdraw_application_error", "Something went wrong while withdrawing acceptance")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Withdraw attendance to an event -// -// @Summary Withdraw attendance after accepting to go to an event. -// @Description Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant. -// @Tags Application -// -// @Param eventId path string true "ID of the event to withdraw attendance from" -// @Success 200 "Attendance withdrawn successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to withdraw attendance" -// @Router /events/{eventId}/application/withdraw-attendance [patch] -func (h *ApplicationHandler) WithdrawAttendance(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - err = h.appService.WithdrawAttendance(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("withdraw_attendance_error", "Something went wrong while withdrawing attendance")) - return - } - w.WriteHeader(http.StatusOK) - -} - -// Accept an Acceptance for an Event/Application -// -// @Summary Accept an acceptance after being accepted to an event. -// @Description Sets event role to attendee, from applicant -// @Tags Application Event -// -// @Param eventId path string true "ID of the event to join the waitlist for" -// @Success 200 "Acceptance successful" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to accept" -// @Router /events/{eventId}/application/accept-acceptance [patch] -func (h *ApplicationHandler) AcceptApplicationAcceptance(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - userId := ctxutils.GetUserIdFromCtx(r.Context()) - - err = h.appService.AcceptApplicationAcceptance(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("accept-acceptance-error", "Something went wrong while accepting acceptance")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Transition waitlisted applications -// -// @Summary Sets application status from accepted to rejected -// @Description Transitions all accepted users to waitlist, and accepts 50 from the waitlist. -// @Tags Application Event -// -// @Param eventId path string true "ID of the event to join the waitlist for" -// @Success 200 "Transitioned application statuses successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses" -// @Router /events/{eventId}/application/transition-waitlisted-applications [patch] -func (h *ApplicationHandler) TransitionWaitlistedApplications(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - var acceptanceCount uint32 = 50 - var acceptanceQuota uint32 = 500 - err = h.appService.TransitionWaitlistedApplications(r.Context(), eventId, acceptanceCount, acceptanceQuota) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("transition-waitlisted-applications-error", "Something went wrong while transitioning waitlisted applications.")) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/apps/api/internal/api/handlers/auth.go b/apps/api/internal/api/handlers/auth.go deleted file mode 100644 index 6c537217..00000000 --- a/apps/api/internal/api/handlers/auth.go +++ /dev/null @@ -1,197 +0,0 @@ -package handlers - -import ( - "encoding/base64" - "encoding/json" - "errors" - "net" - "net/http" - "net/url" - - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/cookie" - "github.com/swamphacks/core/apps/api/internal/services" -) - -type AuthHandler struct { - authService *services.AuthService - cfg *config.Config - logger zerolog.Logger -} - -func NewAuthHandler(authService *services.AuthService, cfg *config.Config, logger zerolog.Logger) *AuthHandler { - return &AuthHandler{ - authService: authService, - cfg: cfg, - logger: logger.With().Str("handler", "AuthHandler").Str("component", "auth").Logger(), - } -} - -// GetMe -// -// @Summary Get Current User​ -// @Description Get the currently authenticated user's information. -// @Tags Authentication -// @Produce json -// @Param sh_session cookie string true "The authenticated session token/id" -// @Success 200 {object} middleware.UserContext -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 500 {object} response.ErrorResponse -// @Router /auth/me [get] [get] -func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) { - user, err := h.authService.GetMe(r.Context()) - if err != nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("no_user", "Your profile could not be loaded.")) - return - } - - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - if err = json.NewEncoder(w).Encode(user); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went seriously wrong.")) - return - } -} - -func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { - err := h.authService.Logout(r.Context()) - if err != nil && errors.Is(err, services.ErrFetchSessionContextFailed) { - res.SendError(w, http.StatusUnauthorized, res.NewError("no_auth", "You are not authorized.")) - return - } else if err != nil && errors.Is(err, services.ErrInvalidateSessionFailed) { - res.SendError(w, http.StatusInternalServerError, res.NewError("logout_err", "Failed to logout of your session")) - return - } else if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went seriously wrong.")) - return - } - - // Invalidate cookie - cookie.ClearSessionCookie(w, h.cfg.Cookie) -} - -// OAuth Callbacks -type OAuthState struct { - Nonce string `json:"nonce"` - Provider string `json:"provider"` - Redirect string `json:"redirect"` -} - -func ensureLeadingSlash(s string) string { - if len(s) == 0 || s[0] != '/' { - return "/" + s - } - return s -} - -func isURL(s string) bool { - u, err := url.Parse(s) - return err == nil && u.Scheme != "" && u.Host != "" -} - -// OAuth2 Auth Callback -// -// @Summary OAuth2 Auth Callback -// @Description This route is used for OAuth authentication methods to verify and login/create an account. -// @Tags Authentication -// @Accept json -// @Produce json -// @Param code query string true "The OAuth code passed back from the provider. Part of the PKCE flow." -// @Param state query string true "The state containing a base64 encoded version of the nonce, provider, and redirect url." -// @Param sh_auth_nonce header string true "The nonce for comparing against the callback state decoded to prevent CSRF attacks." -// @Success 200 "OK: User is logged in successfully" -// @Header 200 {string} Set-Cookie "Sets a sh_session cookie to signify auth status" -// @Success 302 "Found: Logged in and redirected to a requested location" -// @Header 302 {string} Set-Cookie "Sets a sh_session cookie to signify auth status" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Something went wrong with the request queries or their properties" -// @Failure 403 {object} response.ErrorResponse "Forbidden: Something went wrong verifying identity or authenticating." -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Failure 502 {object} response.ErrorResponse "Bad Gateway: Authenticating OAuth server did not respond or user does not exist" -// @Router /auth/callback [post] -func (h *AuthHandler) OAuthCallback(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - - codeParam := q.Get("code") - stateParam := q.Get("state") - - // User Agent + IpAddress for session - var ipAddress *string - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err == nil && ip != "" { - ipAddress = &ip - } - - var userAgent *string - ua := r.Header.Get("User-Agent") - if ua != "" { - userAgent = &ua - } - - // Empty parameters - if codeParam == "" || stateParam == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_callback", "This callback was invalid. Please try again.")) - return - } - - decodedStateBytes, err := base64.URLEncoding.DecodeString(stateParam) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_callback", "This callback was invalid. Please try again.")) - return - } - - var state OAuthState - if err := json.Unmarshal(decodedStateBytes, &state); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_callback", "This callback was invalid. Please try again.")) - return - } - - nonceCookie, err := r.Cookie("sh_auth_nonce") - if err != nil { - if errors.Is(err, http.ErrNoCookie) { - res.SendError(w, http.StatusForbidden, res.NewError("auth_error", "Failed to authenticate. Please try again.")) - return - } - - res.SendError(w, http.StatusBadRequest, res.NewError("bad_cookie", "The cookie jar spilled over 😔")) - return - } - - if nonceCookie.Value != state.Nonce { - res.SendError(w, http.StatusUnauthorized, res.NewError("auth_error", "Failed to authenticate. Please try again.")) - return - } - - // Delete nonce cookie! - cookie.ExpireCookie(w, h.cfg.Cookie, "sh_auth_nonce") - - // At this point, nonce has matched, proceed with remaining authentication services - session, err := h.authService.AuthenticateWithOAuth(r.Context(), codeParam, state.Provider, ipAddress, userAgent) - if err != nil { - switch err { - case services.ErrProviderUnsupported: - res.SendError(w, http.StatusNotImplemented, res.NewError("provider_error", "This provider is not supported... are you sure you're supposed to be here?")) - return - case services.ErrAuthenticationFailed: - res.SendError(w, http.StatusNotImplemented, res.NewError("auth_err", "Failed to authenticate the user.")) - return - default: - h.logger.Err(err).Msg("Something unexpected happened.") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went horribly wrong!")) - return - } - } - - // Redirect path must be a relative path like /dashboard or /settings, not a URL - if isURL(state.Redirect) { - // TODO: We should redirect to the login page and display an error message instead of calling SendError here, same for the other places - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_redirect", "The redirect path is invalid.")) - return - } - - redirectPath := ensureLeadingSlash(state.Redirect) - - cookie.SetSessionCookie(w, session.ID, session.ExpiresAt, h.cfg.Cookie) - http.Redirect(w, r, h.cfg.ClientUrl+redirectPath, http.StatusSeeOther) -} diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go deleted file mode 100644 index 8f403b6f..00000000 --- a/apps/api/internal/api/handlers/bat.go +++ /dev/null @@ -1,225 +0,0 @@ -package handlers - -import ( - "encoding/json" - "errors" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type BatHandler struct { - BatService *services.BatService - logger zerolog.Logger -} - -func NewBatHandler(BatService *services.BatService, logger zerolog.Logger) *BatHandler { - return &BatHandler{ - BatService: BatService, - logger: logger.With().Str("handler", "BatRunHandler").Str("component", "event_interest").Logger(), - } -} - -// Get BatRuns -// -// @Summary Get BatRuns -// @Description Gets BatRuns. -// @Tags Bat -// @Accept json -// @Produce json -// @Success 200 {array} sqlc.GetRunsByEventIdRow "OK: BatRuns returned" -// @Router /events/{eventId}/bat-runs [get] -func (h *BatHandler) GetRunsByEventId(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - runs, err := h.BatService.GetRunsByEventId(r.Context(), eventId) - if errors.Is(err, services.ErrMissingFields) { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: eventId")) - return - } - - if errors.Is(err, services.ErrMissingPerms) { - res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are forbidden from this resource.")) - return - } - - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(runs); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - return - } - -} - -// Check application reviews complete -// -// @Summary Check if application reviews complete -// @Description Check if application reviews complete -// @Tags Bat -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 "OK" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/review-status [get] -func (h *BatHandler) CheckApplicationReviewsComplete(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - reviewsComplete, err := h.BatService.CheckApplicationReviewsComplete(r.Context(), eventId) - if errors.Is(err, services.ErrMissingFields) { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: eventId")) - return - } - - if errors.Is(err, services.ErrMissingPerms) { - res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are forbidden from this resource.")) - return - } - - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(reviewsComplete); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - return - } -} - -// Delete a run -// -// @Summary Delete a run -// @Description Delete an existing BAT run -// @Tags Bat -// @Accept json -// @Produce json -// @Param eventId path string true "Run ID" Format(uuid) -// @Success 204 "OK - Run deleted" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/bat-runs [delete] -func (h *BatHandler) DeleteRunById(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - err = h.BatService.DeleteRunById(r.Context(), eventId) - - if err != nil { - switch err { - case services.ErrFailedToDeleteRun: - res.SendError(w, http.StatusInternalServerError, res.NewError("delete_error", "Failed to delete event")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - } - - w.WriteHeader(http.StatusNoContent) -} - -// Queue transition waitlist task -// -// @Summary Queues a waitlist transition task -// @Description Queues an asynq task that transitions waitlisted applications, running every 3 days. -// @Tags -// -// @Param eventId path string true "ID of the event to join the waitlist for" -// @Success 200 "Transitioned application statuses successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses" -// @Router /events/{eventId}/queue-transition-waitlist-task [post] -func (h *BatHandler) QueueScheduleWaitlistTransitionTask(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - err = h.BatService.QueueScheduleWaitlistTransitionTask(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to create ScheduleWaitlistTransition task.")) - } - - res.Send(w, http.StatusCreated, nil) -} - -// Queue Shutdown scheduler task -// -// @Summary Shutsdown an asynq scheduler -// @Description Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active. -// @Tags -// -// @Success 200 "Scheduler shutdown successfully" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to shutdown scheduler" -// @Router /events/{eventId}/queue-transition-waitlist-task [post] -func (h *BatHandler) QueueShutdownWaitlistSchedulerTask(w http.ResponseWriter, r *http.Request) { - err := h.BatService.QueueShutdownWaitlistScheduler() - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to shutdown scheduler.")) - } - - res.Send(w, http.StatusOK, nil) -} - -// Send welcome emails -// -// @Summary Sends welcome emails to attendees -// @Description -// @Tags -// -// @Param eventId path string true "ID of the event" -// @Success 200 "Welcome emails began to queue successfully" -// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to begin queuing welcome emails" -// @Router /events/{eventId}/send-welcome-emails [post] -func (h *BatHandler) SendWelcomeEmails(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) - return - } - - err = h.BatService.SendWelcomeEmailToAttendees(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to send welcome emails.")) - } - - res.Send(w, http.StatusCreated, nil) -} diff --git a/apps/api/internal/api/handlers/discord.go b/apps/api/internal/api/handlers/discord.go deleted file mode 100644 index 6a045599..00000000 --- a/apps/api/internal/api/handlers/discord.go +++ /dev/null @@ -1,111 +0,0 @@ -package handlers -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/services" -) - -type DiscordHandler struct { - discordService *services.DiscordService - logger zerolog.Logger -} - -func NewDiscordHandler(discordService *services.DiscordService, logger zerolog.Logger) *DiscordHandler { - return &DiscordHandler{ - discordService: discordService, - logger: logger.With().Str("handler", "DiscordHandler").Str("component", "discord").Logger(), - } -} - -// GetEventAttendeesWithDiscord -// -// @Summary Get event attendees with Discord IDs -// @Description Get all attendees for an event who have Discord accounts linked -// @Tags Discord -// @Param event_id path string true "Event ID (UUID)" -// @Success 200 {array} sqlc.GetEventAttendeesWithDiscordRow "List of attendees with Discord IDs" -// @Failure 400 {object} response.ErrorResponse "Invalid event ID" -// @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /discord/event/{event_id}/attendees [get] -func (h *DiscordHandler) GetEventAttendeesWithDiscord(w http.ResponseWriter, r *http.Request) { - eventIDStr := chi.URLParam(r, "event_id") - if eventIDStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "event_id is required")) - return - } - - eventID, err := uuid.Parse(eventIDStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "invalid event ID format")) - return - } - - attendees, err := h.discordService.GetEventAttendeesWithDiscord(r.Context(), eventID) - if err != nil { - h.logger.Err(err).Msg("failed to get event attendees with discord") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to get attendees")) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(attendees); err != nil { - h.logger.Err(err).Msg("failed to encode response") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to encode response")) - return - } -} - -// GetUserEventRoleByDiscordIDAndEventId -// -// @Summary Get user event role by Discord ID and Event ID -// @Description Get the event role for a user based on their Discord account ID and a specific event ID -// @Tags Discord -// @Param eventId path string true "Event ID (UUID)" -// @Param discordId path string true "Discord account ID" -// @Success 200 {object} map[string]interface{} "role" -// @Failure 400 {object} response.ErrorResponse "Invalid event ID or discord ID" -// @Failure 404 {object} response.ErrorResponse "User or role not found" -// @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /events/{eventId}/discord/{discordId} [get] -func (h *DiscordHandler) GetUserEventRoleByDiscordIDAndEventId(w http.ResponseWriter, r *http.Request) { - eventIDStr := chi.URLParam(r, "eventId") - if eventIDStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "eventId is required")) - return - } - - eventID, err := uuid.Parse(eventIDStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "invalid event ID format")) - return - } - - discordID := chi.URLParam(r, "discordId") - if discordID == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_request", "discordId is required")) - return - } - - role, err := h.discordService.GetUserEventRoleByDiscordIDAndEventId(r.Context(), discordID, eventID) - if err != nil { - if err == services.ErrNoEventRole { - res.SendError(w, http.StatusNotFound, res.NewError("not_found", err.Error())) - return - } - h.logger.Err(err).Msg("failed to get user event role") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_error", "Failed to get user role")) - return - } - - response := map[string]interface{}{ - "role": role, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} \ No newline at end of file diff --git a/apps/api/internal/api/handlers/email.go b/apps/api/internal/api/handlers/email.go deleted file mode 100644 index 32947389..00000000 --- a/apps/api/internal/api/handlers/email.go +++ /dev/null @@ -1,164 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/go-playground/validator/v10" - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/services" -) - -type EmailHandler struct { - emailService *services.EmailService - logger zerolog.Logger -} - -func NewEmailHandler(emailService *services.EmailService, logger zerolog.Logger) *EmailHandler { - return &EmailHandler{ - emailService: emailService, - logger: logger.With().Str("handler", "EmailHandler").Str("component", "email").Logger(), - } -} - -type QueueTextEmailRequest struct { - To []string `json:"to"` - Subject string `json:"subject"` - Body string `json:"body"` -} - -// Queue an Email Request -// -// @Summary Queue an Email Request -// @Description Push an email request to the task queue -// @Tags Email -// @Accept json -// @Produce json -// @Param request body QueueTextEmailRequest true "Email data" -// @Success 201 {object} string "OK: Email request queued" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid." -// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending" -// @Router /email/queue [post] -func (h *EmailHandler) QueueTextEmail(w http.ResponseWriter, r *http.Request) { - var req QueueTextEmailRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - - for _, to := range req.To { - if !email.IsValidEmail(to) { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_email", "'To' email is malformed or missing")) - return - } - } - - if req.Subject == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_subject", "Subject is missing or is an empty string.")) - return - } - - if req.Body == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_body", "Body is missing or is an empty string.")) - return - } - - taskInfo, err := h.emailService.QueueSendTextEmail(req.To, req.Subject, req.Body) - if err != nil { - h.logger.Err(err).Msg("Failed to queue SendTextEmail from EmailHandler") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "The server went kaput while queueing email sending")) - return - } - - h.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued SendTextEmail task!") - - w.WriteHeader(http.StatusCreated) -} - -type QueueConfirmationEmailFields struct { - Email string `json:"email" validate:"required"` - FirstName string `json:"firstName" validate:"required"` -} - -// Queue a Confirmation Email -// -// @Summary Queue a Confirmation Email Request -// @Description Push a Confirmation Email request to the task queue -// @Tags Email -// @Accept json -// @Produce json -// @Param request body QueueConfirmationEmailFields true "Email data" -// @Success 201 {object} string "OK: Email request queued" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid." -// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending" -// @Router /email/queue [post] -func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) { - var req QueueConfirmationEmailFields - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() - err := decoder.Decode(&req) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - validate := validator.New() - if err := validate.Struct(req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) - } - - err = h.emailService.QueueConfirmationEmail(req.Email, req.FirstName) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Confirmation email could not be queued.")) - } - - res.Send(w, http.StatusOK, nil) -} - -type QueueWelcomeEmailFields struct { - Email string `json:"email" validate:"required"` - FirstName string `json:"firstName" validate:"required"` - UserId string `json:userId validate:"required"` -} - -// Queue a Welcome Email -// -// @Summary Queue a Welcome Email -// @Description Push an Welcome Email request to the task queue -// @Tags Email -// @Accept json -// @Produce json -// @Param request body QueueConfirmationEmailFields true "Email data" -// @Success 201 {object} string "OK: Email request queued" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request. The email request is potentially invalid." -// @Failure 500 {object} response.ErrorResponse "Server Error: The server went kaput while queueing email sending" -func (h *EmailHandler) QueueWelcomeEmail(w http.ResponseWriter, r *http.Request) { - var req QueueWelcomeEmailFields - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() - err := decoder.Decode(&req) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) - } - - parsedUserId, err := uuid.Parse(req.UserId) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "userId must be of type uuid")) - return - } - - err = h.emailService.QueueWelcomeEmail(r.Context(), req.Email, req.FirstName, parsedUserId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Hacker email could not be queued.")) - } - - res.Send(w, http.StatusOK, nil) -} diff --git a/apps/api/internal/api/handlers/event_interest.go b/apps/api/internal/api/handlers/event_interest.go deleted file mode 100644 index e4b88171..00000000 --- a/apps/api/internal/api/handlers/event_interest.go +++ /dev/null @@ -1,88 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/services" -) - -type EventInterestHandler struct { - eventInterestService *services.EventInterestService - cfg *config.Config - logger zerolog.Logger -} - -func NewEventInterestHandler(eventInterestService *services.EventInterestService, cfg *config.Config, logger zerolog.Logger) *EventInterestHandler { - return &EventInterestHandler{ - eventInterestService: eventInterestService, - cfg: cfg, - logger: logger.With().Str("handler", "EventInterestHandler").Str("component", "event_interest").Logger(), - } -} - -// AddEmailRequest is the expected payload for adding an email -type AddEmailRequest struct { - Email string `json:"email"` - Source *string `json:"source"` -} - -// Make an interest submission for an event -// -// @Summary Make an interest submission for an event (email list) -// @Description Submit email for event interest/mailing list -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" -// @Param request body AddEmailRequest true "Interest submission data" -// @Success 201 {object} string "OK: Interest email created" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request" -// @Failure 409 {object} response.ErrorResponse "Duplicate email found in DB" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/interest [post] -func (h *EventInterestHandler) AddEmailToEvent(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - // Parse JSON body - var req AddEmailRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - - if !email.IsValidEmail(req.Email) { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_email", "Email is required")) - return - } - - _, err = h.eventInterestService.CreateInterestSubmission(r.Context(), eventId, req.Email, req.Source) - if err != nil { - switch err { - case services.ErrEmailConflict: - res.SendError(w, http.StatusConflict, res.NewError("duplicate_email", "Email already subscribed for updates")) - case services.ErrFailedToCreateSubmission: - res.SendError(w, http.StatusInternalServerError, res.NewError("submission_error", "Failed to create event interest submission")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - return - } - - w.WriteHeader(http.StatusCreated) -} diff --git a/apps/api/internal/api/handlers/events.go b/apps/api/internal/api/handlers/events.go deleted file mode 100644 index 335e8dff..00000000 --- a/apps/api/internal/api/handlers/events.go +++ /dev/null @@ -1,961 +0,0 @@ -package handlers - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "time" - - "github.com/go-chi/chi/v5" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/parse" - . "github.com/swamphacks/core/apps/api/internal/parse" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type EventHandler struct { - eventService *services.EventService - cfg *config.Config - logger zerolog.Logger -} - -func NewEventHandler(eventService *services.EventService, cfg *config.Config, logger zerolog.Logger) *EventHandler { - return &EventHandler{ - eventService: eventService, - cfg: cfg, - logger: logger.With().Str("handler", "EventHandler").Str("component", "events").Logger(), - } -} - -// Be very careful with the types in this struct. If a type is not a pointer (pointer types allow a null value), and the field is not present in the json body, its default value will be passed to the SQL query, and be a non-null value will be put into coalese(), which will then make a NULL value impossible and instead make the default value the type's zero value in Go. -type CreateEventFields struct { - Name string `json:"name" validate:"required,min=5,max=30"` - ApplicationOpen time.Time `json:"application_open" validate:"required"` - ApplicationClose time.Time `json:"application_close" validate:"required"` - StartTime time.Time `json:"start_time" validate:"required"` - EndTime time.Time `json:"end_time" validate:"required"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` -} - -func (st CreateEventFields) ValidateTimeFields() bool { - if st.ApplicationClose.Before(st.ApplicationOpen) || st.ApplicationClose.Equal(st.ApplicationOpen) { - return false - } - if st.EndTime.Before(st.StartTime) || st.EndTime.Equal(st.StartTime) { - return false - } - return true -} - -// Create a new event -// -// @Summary Create a new event -// @Description Create a new event with the provided details -// @Tags Event -// @Accept json -// @Produce json -// @Param request body CreateEventFields true "Event creation data" -// @Success 201 {object} sqlc.Event "OK: Event created" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request" -// @Failure 409 {object} response.ErrorResponse "endTime is before startTime or applicationClose is before applicationOpen" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events [post] -func (h *EventHandler) CreateEvent(w http.ResponseWriter, r *http.Request) { - - // Parse JSON body - var req CreateEventFields - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - // This will also throw an error for empty values for fields which correspond to types that cannot convert an empty string to a zero value (e.g. time.Time) - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) - } - - if !req.ValidateTimeFields() { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_time", "Time fields must be sequential and not in the past.")) - return - } - - params := sqlc.CreateEventParams{ - Name: req.Name, - ApplicationOpen: req.ApplicationOpen, - ApplicationClose: req.ApplicationClose, - StartTime: req.StartTime, - EndTime: req.EndTime, - Description: req.Description, - Location: req.Location, - LocationUrl: req.LocationUrl, - MaxAttendees: req.MaxAttendees, - RsvpDeadline: req.RsvpDeadline, - DecisionRelease: req.DecisionRelease, - WebsiteUrl: req.WebsiteUrl, - IsPublished: req.IsPublished, - } - - event, err := h.eventService.CreateEvent(r.Context(), params) - if err != nil { - if errors.Is(err, services.ErrFailedToCreateEvent) { - res.SendError(w, http.StatusInternalServerError, res.NewError("creation_error", "Failed to create event")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - } - - res.Send(w, http.StatusCreated, event) -} - -// Get an event -// -// @Summary Get an event -// @Description Get a specific event by ID -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 201 {object} sqlc.Event "OK - Event received" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId} [get] -func (h *EventHandler) GetEventByID(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - event, err := h.eventService.GetEventByID(r.Context(), eventId) - if err != nil { - switch err { - case services.ErrFailedToGetEvent: - res.SendError(w, http.StatusNotFound, res.NewError("no_event", "Event not found")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - } - - res.Send(w, http.StatusOK, event) -} - -type UpdateEventFields struct { - Name Optional[string] `json:"name"` - Description Optional[*string] `json:"description"` - Location Optional[*string] `json:"location"` - LocationUrl Optional[*string] `json:"location_url"` - MaxAttendees Optional[*int32] `json:"max_attendees"` - ApplicationOpen Optional[time.Time] `json:"application_open"` - ApplicationClose Optional[time.Time] `json:"application_close"` - RsvpDeadline Optional[*time.Time] `json:"rsvp_deadline"` - DecisionRelease Optional[*time.Time] `json:"decision_release"` - StartTime Optional[time.Time] `json:"start_time"` - EndTime Optional[time.Time] `json:"end_time"` - WebsiteUrl Optional[*string] `json:"website_url"` - IsPublished Optional[bool] `json:"is_published"` -} - -// Update an event -// -// @Summary Update an event -// @Description Update an existing event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 204 "OK - Event updated (patched)" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId} [patch] -func (h *EventHandler) UpdateEventById(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - var req UpdateEventFields - - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) - return - } - - var params = sqlc.UpdateEventByIdParams{ - NameDoUpdate: req.Name.Present, - Name: req.Name.Value, - - DescriptionDoUpdate: req.Description.Present, - Description: req.Description.Value, - - LocationDoUpdate: req.Location.Present, - Location: req.Location.Value, - - LocationUrlDoUpdate: req.LocationUrl.Present, - LocationUrl: req.LocationUrl.Value, - - MaxAttendeesDoUpdate: req.MaxAttendees.Present, - MaxAttendees: req.MaxAttendees.Value, - - ApplicationOpenDoUpdate: req.ApplicationOpen.Present, - ApplicationOpen: req.ApplicationOpen.Value, - - ApplicationCloseDoUpdate: req.ApplicationClose.Present, - ApplicationClose: req.ApplicationClose.Value, - - RsvpDeadlineDoUpdate: req.RsvpDeadline.Present, - RsvpDeadline: req.RsvpDeadline.Value, - - DecisionReleaseDoUpdate: req.DecisionRelease.Present, - DecisionRelease: req.DecisionRelease.Value, - - StartTimeDoUpdate: req.StartTime.Present, - StartTime: req.StartTime.Value, - - EndTimeDoUpdate: req.EndTime.Present, - EndTime: req.EndTime.Value, - - WebsiteUrlDoUpdate: req.WebsiteUrl.Present, - WebsiteUrl: req.WebsiteUrl.Value, - - IsPublishedDoUpdate: req.IsPublished.Present, - IsPublished: &req.IsPublished.Value, - - BannerDoUpdate: false, // Banners are uploaded using a separate endpoint - Banner: nil, - - ID: eventId, - } - - event, err := h.eventService.UpdateEventById(r.Context(), params) - - if err != nil { - switch err { - case services.ErrFailedToUpdateEvent: - res.SendError(w, http.StatusInternalServerError, res.NewError("patch_error", "Failed to update event")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - } - - res.Send(w, http.StatusOK, event) -} - -type NullableEventRole struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` - Role *sqlc.EventRoleType `json:"role"` - AssignedAt *time.Time `json:"assigned_at"` -} - -// Get the current user's event role for an event -// -// @Summary Get the current user's event role for an event -// @Description Get current user's role for a specific event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 {object} NullableEventRole "OK - Return role" -// @Failure 400 {object} response.ErrorResponse "Not Found - Role not found" -// @Failure 404 {object} response.ErrorResponse "Not Found - Role not found" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/role [get] -func (h *EventHandler) GetEventRole(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - eventRole, err := h.eventService.GetEventRoleByIds(r.Context(), *userId, eventId) - if err != nil { - if errors.Is(err, repository.ErrEventRoleNotFound) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(NullableEventRole{ - UserID: *userId, - EventID: eventId, - Role: nil, - AssignedAt: nil, - }) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went terribly wrong.")) - } - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(NullableEventRole{ - UserID: eventRole.UserID, - EventID: eventRole.EventID, - Role: &eventRole.Role, - AssignedAt: eventRole.AssignedAt, - }) -} - -// Delete an event -// -// @Summary Delete an event -// @Description Delete an existing event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 204 "OK - Event deleted" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId} [delete] -func (h *EventHandler) DeleteEventById(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - err = h.eventService.DeleteEventById(r.Context(), eventId) - - if err != nil { - switch err { - case services.ErrFailedToDeleteEvent: - res.SendError(w, http.StatusInternalServerError, res.NewError("delete_error", "Failed to delete event")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - } - } - - w.WriteHeader(http.StatusNoContent) -} - -// Get events -// -// @Summary Get events -// @Description Gets events with a nullable event role for authenticated users. -// @Tags Event -// @Accept json -// @Produce json -// @Param scope query string false "Can be scoped to either published, scoped, or all. Scoped means admins and staff can see unpublished events" default("published") -// @Success 200 {array} sqlc.GetEventsWithUserInfoRow "OK: Events returned" -// @Router /events [get] -func (h *EventHandler) GetEvents(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - scope, err := parse.ParseGetEventScopeType(q.Get("scope")) - - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: include_unpublished=published, scoped, all, or none (default to published)")) - return - } - - events, err := h.eventService.GetEvents(r.Context(), scope) - if errors.Is(err, services.ErrMissingFields) { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: include_unpublished=published, scoped, all, or none (default to published)")) - return - } - - if errors.Is(err, services.ErrMissingPerms) { - res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are forbidden from this resource.")) - return - } - - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(events); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - return - } -} - -// Get all staff users for an event -// -// @Summary Get all staff users for an event -// @Description Gets all users with role STAFF or ADMIN -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 {array} sqlc.GetEventStaffRow "OK - Return users" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/staff [get] -func (h *EventHandler) GetEventStaffUsers(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - users, err := h.eventService.GetEventStaffUsers(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(users); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - return - } -} - -// Get all users for an event -// -// @Summary Get all users for an event -// @Description Gets all users with any role for the event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Success 200 {array} sqlc.GetEventStaffRow "OK - Return users" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/users [get] -func (h *EventHandler) GetEventUsers(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - users, err := h.eventService.GetEventUsers(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(users); err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) - return - } -} - -type AssignRoleFields struct { - Email *string `json:"email"` - UserID *string `json:"user_id"` - Role sqlc.EventRoleType `json:"role"` -} - -func (r *AssignRoleFields) Validate() error { - if r.Email != nil && !email.IsValidEmail(*r.Email) { - return fmt.Errorf("invalid email: %s", *r.Email) - } - - switch r.Role { - case sqlc.EventRoleTypeAdmin, sqlc.EventRoleTypeStaff, sqlc.EventRoleTypeAttendee, sqlc.EventRoleTypeApplicant: - return nil - default: - return fmt.Errorf("invalid role: %q", r.Role) - } -} - -// Change or add event role of a user -// -// @Summary Change or add event role of a user -// @Description Modify user's role for a specific event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param request body AssignRoleFields true "Event role data" -// @Success 200 "OK - Role updated" -// @Failure 404 {object} response.ErrorResponse "Not Found - User not found" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/roles [post] -func (h *EventHandler) AssignEventRole(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - var input AssignRoleFields - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_body", err.Error())) - return - } - - if err := input.Validate(); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_body", err.Error())) - return - } - - userId := parse.ParseUUIDOrNil(input.UserID) - email := parse.ParseStrToPtr(input.Email) - - err = h.eventService.AssignEventRole(r.Context(), userId, email, eventId, input.Role) - if err != nil { - switch err { - case repository.ErrUserNotFound: - res.SendError(w, http.StatusNotFound, res.NewError("user_missing", "The user does not exist")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - } - return - } - - w.WriteHeader(http.StatusOK) -} - -type AssignRoleBatch struct { - Assignments []AssignRoleFields `json:"assignments"` -} - -func (b *AssignRoleBatch) Validate() error { - if len(b.Assignments) == 0 { - return fmt.Errorf("at least one assignment is required") - } - for i, a := range b.Assignments { - if err := a.Validate(); err != nil { - return fmt.Errorf("assignment[%d]: %w", i, err) - } - } - return nil -} - -// Change or add event role of a user in batch -// -// @Summary Change or add event role of a user in batch -// @Description Modify users' role for a specific event -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param request body AssignRoleBatch true "Event roles data" -// @Success 200 "OK - Roles updated" -// @Failure 404 {object} response.ErrorResponse "Not Found - User not found" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/roles/batch [post] -func (h *EventHandler) BatchAssignEventRoles(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - var input AssignRoleBatch - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_body", err.Error())) - return - } - - if err := input.Validate(); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_body", err.Error())) - return - } - - for _, assignment := range input.Assignments { - userId := parse.ParseUUIDOrNil(assignment.UserID) - email := parse.ParseStrToPtr(assignment.Email) - - err = h.eventService.AssignEventRole(r.Context(), userId, email, eventId, assignment.Role) - if err != nil { - switch err { - case repository.ErrUserNotFound: - res.SendError(w, http.StatusNotFound, res.NewError("user_missing", fmt.Sprintf("The user %v does not exist", assignment.UserID))) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - } - return - } - } - - w.WriteHeader(http.StatusOK) -} - -// Revoke event role of a user -// -// @Summary Revoke event role of a user -// @Description Remove user's role for a specific event -// @Tags Event -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param userId path string true "User ID" Format(uuid) -// @Success 200 "OK - Role revoked" -// @Failure 404 {object} response.ErrorResponse "Not Found - User not found" -// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." -// @Router /events/{eventId}/roles/{userId} [delete] -func (h *EventHandler) RevokeEventRole(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - userIdStr := chi.URLParam(r, "userId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - if userIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!")) - return - } - - userId, err := uuid.Parse(userIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "The user ID is not a valid UUID")) - return - } - - err = h.eventService.RevokeEventRole(r.Context(), userId, eventId) - if err != nil { - switch err { - case repository.ErrUserNotFound: - res.SendError(w, http.StatusNotFound, res.NewError("user_missing", "The user does not exist")) - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - } - return - } - - w.WriteHeader(http.StatusOK) -} - -func deferredCloser(c io.Closer, name string) func() { - return func() { - if err := c.Close(); err != nil { - log.Err(err).Msg("Failed to close " + name) - } - } -} - -const maxBannerUploadSize = 5 << 20 // 5 Mb - -type EventBannerUploadResponse struct { - BannerUrl string `json:"banner_url"` -} - -func (h *EventHandler) UploadEventBanner(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - if err := r.ParseMultipartForm(maxBannerUploadSize); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "could not parse multipart form")) - return - } - - bannerFile, header, err := r.FormFile("image") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "invalid resume file")) - return - } - defer deferredCloser(bannerFile, "banner file") - - url, err := h.eventService.UploadBanner(r.Context(), eventId, bannerFile, header) - switch err { - case services.ErrFailedToUploadBanner: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - return - case services.ErrUnexpectedFileType: - res.SendError(w, http.StatusBadRequest, res.NewError("file_error", err.Error())) - return - case nil: - // Continue - default: - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - return - } - - res.Send(w, http.StatusOK, EventBannerUploadResponse{ - BannerUrl: *url, - }) - -} - -func (h *EventHandler) DeleteBanner(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - err = h.eventService.DeleteBanner(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong on our end")) - return - } - - w.WriteHeader(http.StatusOK) -} - -// Get Event Overview -// -// @Summary Retrieves general information about the event -// @Description Returns data such as event details (name, description, location, dates, etc..) and basic application statistics -// @Tags Event -// @Produce json -// @Success 200 {object} services.EventOverview -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error getting statistics" -// @Router /events/{eventId}/overview [get] -func (h *EventHandler) GetEventOverview(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID.")) - return - } - - eventStats, err := h.eventService.GetEventOverview(r.Context(), eventId) - - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("event_overview_err", "Something went wrong while aggregating event statistics.")) - return - } - - res.Send(w, http.StatusOK, eventStats) -} - -// Get User for Event -// -// @Summary Retrieves a user's information along with their event information -// @Description A user's information along with their event details such as check in state, role, and more. Must be validated on the frontend. -// @Tags Event -// @Produce json -// @Success 200 {object} services.UserInfoForEvent -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user info for event" -// @Router /events/{eventId}/users/{userId} [get] -func (h *EventHandler) GetUserForEvent(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - userId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!")) - return - } - - info, err := h.eventService.GetUserInfoForEvent(r.Context(), userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("user_info_err", "Something went wrong while querying user info for event")) - return - } - - res.Send(w, http.StatusOK, info) -} - -// Get User by RFID -// -// @Summary Retrieves a user's ID by their RFID -// @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. -// @Tags Event -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param rfid path string true "RFID code (10 digits)" -// @Success 200 {object} map[string]string "OK - Returns user ID" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID" -// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID" -// @Router /events/{eventId}/users/by-rfid/{rfid} [get] -func (h *EventHandler) GetUserByRFID(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - rfid := chi.URLParam(r, "rfid") - if rfid == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_rfid", "The RFID is missing from the URL!")) - return - } - - user, err := h.eventService.GetUserByRFID(r.Context(), eventId, rfid) - if err != nil { - if errors.Is(err, repository.ErrUserNotFound) || errors.Is(err, repository.ErrEventRoleNotFound) { - res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "No user found with the provided RFID")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong while querying user by RFID")) - } - return - } - - // Return just the user ID as a simple JSON object - res.Send(w, http.StatusOK, map[string]string{"user_id": user.ID.String()}) -} - -// Get User by RFID -// -// @Summary Retrieves a user's ID by their RFID -// @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. -// @Tags Event -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param rfid path string true "RFID code (10 digits)" -// @Success 200 {object} map[string]string "OK - Returns user ID" -// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request." -// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID" -// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID" -// @Router /events/{eventId}/users/by-rfid/{rfid} [get] -func (h *EventHandler) GetCheckedInStatusByIds(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - userId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!")) - return - } - result, err := h.eventService.GetCheckedInStatusByIds(r.Context(), userId, eventId) - if err != nil { - res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally.")) - return - } - // Return just the checked in status as a simple JSON object - res.Send(w, http.StatusOK, map[string]string{ - "checked_in_status": strconv.FormatBool(result), - }) -} - -type UpdateRFID struct { - RFID string `json:"rfid"` -} - -// UpdateUserRFID -// -// @Summary Updates a user's RFID tag -// @Description Associates a new RFID string with a specific user for the given event. This overwrites any existing RFID association. -// @Tags Event -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID" Format(uuid) -// @Param userId path string true "User ID" Format(uuid) -// @Param body body UpdateRFID true "New RFID data" -// @Success 204 "No Content - RFID updated successfully" -// @Failure 400 {object} response.ErrorResponse "Invalid request body or UUID format" -// @Failure 404 {object} response.ErrorResponse "User or Event not found" -// @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /events/{eventId}/users/{userId}/update-rfid [post] -func (h *EventHandler) UpdateUserRFID(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - - userId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!")) - return - } - - var payload UpdateRFID - err = json.NewDecoder(r.Body).Decode(&payload) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("body_malformed", "Invalid body")) - return - } - defer r.Body.Close() - - if payload.RFID == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("body_malformed", "Invalid body")) - return - } - - tempRole := sqlc.EventRoleTypeAttendee - - err = h.eventService.UpdateEventRoleByIds(r.Context(), userId, eventId, &tempRole, nil, &payload.RFID) - if err != nil { - res.SendError(w, http.StatusNotFound, res.NewError("error", "Something went wrong internally.")) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/apps/api/internal/api/handlers/handlers.go b/apps/api/internal/api/handlers/handlers.go deleted file mode 100644 index 2443a689..00000000 --- a/apps/api/internal/api/handlers/handlers.go +++ /dev/null @@ -1,50 +0,0 @@ -package handlers - -import ( - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/services" -) - -type Handlers struct { - Auth *AuthHandler - User *UserHandler - EventInterest *EventInterestHandler - Event *EventHandler - Email *EmailHandler - Application *ApplicationHandler - Teams *TeamHandler - Admission *AdmissionHandler - Bat *BatHandler - Redeemables *RedeemablesHandler - Discord *DiscordHandler -} - -func NewHandlers( - authService *services.AuthService, - userService *services.UserService, - eventInterestService *services.EventInterestService, - eventService *services.EventService, - emailService *services.EmailService, - appService *services.ApplicationService, - teamService *services.TeamService, - batService *services.BatService, - redeemablesService *services.RedeemablesService, - discordService *services.DiscordService, - cfg *config.Config, - logger zerolog.Logger, -) *Handlers { - return &Handlers{ - Auth: NewAuthHandler(authService, cfg, logger), - User: NewUserHandler(userService, logger), - EventInterest: NewEventInterestHandler(eventInterestService, cfg, logger), - Event: NewEventHandler(eventService, cfg, logger), - Email: NewEmailHandler(emailService, logger), - Application: NewApplicationHandler(appService), - Teams: NewTeamHandler(teamService, logger), - Admission: NewAdmissionHandler(batService, logger), - Bat: NewBatHandler(batService, logger), - Redeemables: NewRedeemablesHandler(redeemablesService, cfg, logger), - Discord: NewDiscordHandler(discordService, logger), - } -} diff --git a/apps/api/internal/api/handlers/redeemables.go b/apps/api/internal/api/handlers/redeemables.go deleted file mode 100644 index 5178b416..00000000 --- a/apps/api/internal/api/handlers/redeemables.go +++ /dev/null @@ -1,257 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type RedeemablesHandler struct { - redeemablesService *services.RedeemablesService - cfg *config.Config - logger zerolog.Logger -} - -func NewRedeemablesHandler( - redeemablesService *services.RedeemablesService, - cfg *config.Config, - logger zerolog.Logger, -) *RedeemablesHandler { - return &RedeemablesHandler{ - redeemablesService: redeemablesService, - cfg: cfg, - logger: logger, - } -} - -// GetRedeemables -// -// @Summary Get all redeemables for an event -// @Description Retrieve a list of all redeemable items associated with a specific event ID. -// @Tags Redeemables -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID (UUID)" -// @Success 200 {array} sqlc.Redeemable -// @Failure 400 {object} response.ErrorResponse "Missing or invalid Event ID" -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Router /events/{eventId}/redeemables [get] -func (h *RedeemablesHandler) GetRedeemables(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing event_id."), - ) - return - } - - redeemables, err := h.redeemablesService.GetRedeemablesByEventID(r.Context(), eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to get redeemables by ID")) - return - } - - res.Send(w, http.StatusOK, redeemables) -} - -type CreateRedeemableRequest struct { - Name string `json:"name"` - Amount int `json:"amount"` - MaxUserAmount int `json:"max_user_amount"` -} - -// CreateRedeemable -// -// @Summary Create a new redeemable -// @Description Create a new redeemable item for a specific event. -// @Tags Redeemables -// @Accept json -// @Produce json -// @Param eventId path string true "Event ID (UUID)" -// @Param request body CreateRedeemableRequest true "Redeemable creation data" -// @Success 201 {object} sqlc.Redeemable -// @Failure 400 {object} response.ErrorResponse "Invalid request body or ID" -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Router /events/{eventId}/redeemables [post] -func (h *RedeemablesHandler) CreateRedeemable(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing event_id."), - ) - return - } - - var req CreateRedeemableRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error())) - return - } - - if req.Name == "" || req.Amount <= 0 { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_params", "name and amount are required")) - return - } - - redeemable, err := h.redeemablesService.CreateRedeemable(r.Context(), eventId, req.Name, req.Amount, req.MaxUserAmount) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to create redeemable")) - return - } - - res.Send(w, http.StatusCreated, redeemable) -} - -type UpdateRedeemableRequest struct { - Name *string `json:"name,omitempty"` - Amount *int `json:"total_stock,omitempty"` - MaxUserAmount *int `json:"max_user_amount,omitempty"` -} - -// UpdateRedeemable -// -// @Summary Update an existing redeemable -// @Description Update specific fields (name, stock, max per user) of a redeemable. -// @Tags Redeemables -// @Accept json -// @Produce json -// @Param redeemableId path string true "Redeemable ID (UUID)" -// @Param request body UpdateRedeemableRequest true "Redeemable update data (partial fields allowed)" -// @Success 200 {object} sqlc.Redeemable -// @Failure 400 {object} response.ErrorResponse "Invalid ID or request body" -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Router /redeemables/{redeemableId} [patch] -func (h *RedeemablesHandler) UpdateRedeemable(w http.ResponseWriter, r *http.Request) { - redeemableId, err := web.PathParamToUUID(r, "redeemableId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing redeemable_id."), - ) - return - } - - var req UpdateRedeemableRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error())) - return - } - - redeemable, err := h.redeemablesService.UpdateRedeemable(r.Context(), redeemableId, req.Name, req.Amount, req.MaxUserAmount) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to update redeemable")) - return - } - - res.Send(w, http.StatusCreated, redeemable) - return -} - -// DeleteRedeemable -// -// @Summary Delete a redeemable -// @Description Permanently delete a redeemable item by ID. -// @Tags Redeemables -// @Param redeemableId path string true "Redeemable ID (UUID)" -// @Success 204 "No Content" -// @Failure 400 {object} response.ErrorResponse "Invalid Redeemable ID" -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Router /redeemables/{redeemableId} [delete] -func (h *RedeemablesHandler) DeleteRedeemable(w http.ResponseWriter, r *http.Request) { - redeemableId, err := web.PathParamToUUID(r, "redeemableId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing redeemable_id."), - ) - return - } - - err = h.redeemablesService.DeleteRedeemable(r.Context(), redeemableId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to delete redeemable")) - return - } - res.Send(w, http.StatusNoContent, nil) -} - -// RedeemRedeemable -// -// @Summary Redeem an item for a user -// @Description Create a redemption record linking a specific user to a redeemable item. -// @Tags Redeemables -// @Param redeemableId path string true "Redeemable ID (UUID)" -// @Param userId path string true "User ID (UUID)" -// @Success 204 "No Content" -// @Failure 400 {object} response.ErrorResponse "Invalid IDs" -// @Failure 500 {object} response.ErrorResponse "Internal Server Error" -// @Router /redeemables/{redeemableId}/users/{userId} [post] -func (h *RedeemablesHandler) RedeemRedeemable(w http.ResponseWriter, r *http.Request) { - // user id, redeemable id - userId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing user_id."), - ) - return - } - - redeemableId, err := web.PathParamToUUID(r, "redeemableId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing redeemable_id."), - ) - return - } - - err = h.redeemablesService.RedeemRedeemable(r.Context(), redeemableId, userId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to redeem redeemable")) - return - } - res.Send(w, http.StatusNoContent, nil) -} - -type UpdateRedemptionRequest struct { - Amount int `json:"new_amount,omitempty"` -} - -func (h *RedeemablesHandler) UpdateRedemption(w http.ResponseWriter, r *http.Request) { - // user id, redeemable id - userId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing user_id."), - ) - return - } - - redeemableId, err := web.PathParamToUUID(r, "redeemableId") - if err != nil { - res.SendError(w, http.StatusBadRequest, - res.NewError("invalid_request", "Invalid or missing redeemable_id."), - ) - return - } - var req UpdateRedemptionRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error())) - return - } - - err = h.redeemablesService.UpdateRedemption(r.Context(), redeemableId, userId, req.Amount) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to update redemption")) - return - } - res.Send(w, http.StatusNoContent, nil) -} diff --git a/apps/api/internal/api/handlers/teams.go b/apps/api/internal/api/handlers/teams.go deleted file mode 100644 index 0e4c1c25..00000000 --- a/apps/api/internal/api/handlers/teams.go +++ /dev/null @@ -1,557 +0,0 @@ -package handlers - -import ( - "encoding/json" - "errors" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/ptr" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type TeamHandler struct { - teamService *services.TeamService - logger zerolog.Logger -} - -func NewTeamHandler(teamService *services.TeamService, logger zerolog.Logger) *TeamHandler { - return &TeamHandler{ - teamService: teamService, - logger: logger.With().Str("handler", "TeamHandler").Str("component", "team").Logger(), - } -} - -// Get the authenticated user's team for this event, including its members. -// -// @Summary Get the authenticated user's team and its members for this specific event. -// @Description Retrieves the team information and the full list of team members for the currently authenticated user within a specified event. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param event_id path int true "The ID of the event" -// @Success 200 {object} services.TeamWithMembers "Team information and members successfully retrieved." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 404 {object} response.ErrorResponse "Team not found for the user in this event." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/teams/me [get] -func (h *TeamHandler) GetMyTeam(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - teamWithMembers, err := h.teamService.GetUserTeamWithMembers(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - if teamWithMembers == nil { - res.SendError(w, http.StatusNotFound, res.NewError("no_team", "user does not have a team for this event")) - return - } - - res.Send(w, http.StatusOK, teamWithMembers) -} - -// Get team by ID -// -// @Summary Get a team and its members by team id. -// @Description Retrieves the team information and the full list of team members by a team id. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Success 200 {object} services.TeamWithMembers "Team information and members successfully retrieved." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 404 {object} response.ErrorResponse "Team not found for the user in this event." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /teams/{teamId} [get] -func (h *TeamHandler) GetTeam(w http.ResponseWriter, r *http.Request) { - teamIdStr := chi.URLParam(r, "teamId") - if teamIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_team_id", "The team ID is missing from the URL!")) - return - } - teamId, err := uuid.Parse(teamIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_team_id", "The team ID is not a valid UUID")) - return - } - - teamWithMembers, err := h.teamService.GetTeamWithMembers(r.Context(), teamId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - if teamWithMembers == nil { - res.SendError(w, http.StatusNotFound, res.NewError("not_found", "could not find the specified team")) - return - } - - res.Send(w, http.StatusOK, teamWithMembers) -} - -// Gets an events teams -// -// @Summary Get an event's teams -// @Description Gets all teams for a specific event. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param event_id path string true "The ID of the event" -// @Success 200 {array} services.TeamWithMembers "Teams successfully retrieved." -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/teams [get] -func (h *TeamHandler) GetEventTeams(w http.ResponseWriter, r *http.Request) { - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - // Parse limit and offset from query parameters - query := r.URL.Query() - limit, err := web.ParseParamInt32(query, "limit", ptr.Int32ToPtr(10)) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_params", "Malformed `limit` query parameter.")) - return - } - - offset, err := web.ParseParamInt32(query, "offset", ptr.Int32ToPtr(0)) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("bad_params", "Malformed `offset` query parameter.")) - return - } - - teams, err := h.teamService.GetTeamsWithMembersByEvent(r.Context(), eventId, *limit, *offset) - if err != nil { - h.logger.Err(err).Msg("Failed to get teams for event") - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - res.Send(w, http.StatusOK, teams) -} - -type CreateTeamRequest struct { - Name string `json:"name"` -} - -// Create a new team -// -// @Summary Create a new team -// @Description Creates a new team for a specific event and assigns the creator as the owner. -// @Tags Team -// -// @Accept json -// @Produce json -// -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param event_id path int true "The ID of the event" -// -// @Param request body CreateTeamRequest true "Team Creation Payload" -// -// @Success 200 {object} sqlc.Team "A team object" -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// -// @Failure 400 {object} response.ErrorResponse "Bad request: you had request parameters needed for this method." -// @Failure 409 {object} response.ErrorResponse "Conflict: You already have a team." -// -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/teams [post] -func (h *TeamHandler) CreateTeam(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - eventIdStr := chi.URLParam(r, "eventId") - if eventIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) - return - } - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - // Extract from body - var body CreateTeamRequest - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Your request is missing a field")) - return - } - defer r.Body.Close() - - team, err := h.teamService.CreateTeam(r.Context(), body.Name, eventId, *userId) - if err != nil { - if errors.Is(err, services.ErrTeamExists) { - res.SendError(w, http.StatusConflict, res.NewError("team_exists", "the current user already has a team!")) - return - } - - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - res.Send(w, http.StatusOK, team) -} - -// Leave a team -// -// @Summary Leave a team -// @Description Leaves a team if the requester is on the team. Depends on cookies for user retrieval. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Success 204 "Successfully left the team" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /teams/{teamId}/members/me [delete] -func (h *TeamHandler) LeaveTeam(w http.ResponseWriter, r *http.Request) { - teamIdStr := chi.URLParam(r, "teamId") - if teamIdStr == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_team_id", "The team ID is missing from the URL!")) - return - } - teamId, err := uuid.Parse(teamIdStr) - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_team_id", "The team ID is not a valid UUID")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - err = h.teamService.LeaveTeam(r.Context(), *userId, teamId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - - } -} - -type CreateJoinRequest struct { - Message *string `json:"message"` -} - -// Request to join a team -// -// @Summary Request to join a team -// @Description Requests to join a team or fails if user is already on a team. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Param event_id path string true "The ID of the event" -// @Param request body CreateJoinRequest true "Team Creation Payload" -// @Accept json -// @Produce json -// @Success 204 "Successfully left the team" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// -// @Failure 409 {object} response.ErrorResponse "Conflict: User is already on a team." -// -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/teams/{teamId}/join [post] -func (h *TeamHandler) RequestToJoinTeam(w http.ResponseWriter, r *http.Request) { - teamId, err := web.PathParamToUUID(r, "teamId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_team_id", "The team ID is malformed/missing.")) - return - } - - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_event_id", "The event ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - var body CreateJoinRequest - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Your request is missing a field")) - return - } - defer r.Body.Close() - - request, err := h.teamService.RequestToJoinTeam(r.Context(), eventId, teamId, *userId, body.Message) - if err != nil { - if errors.Is(err, services.ErrUserOnTeam) { - res.SendError(w, http.StatusConflict, res.NewError("already_teamed", "You are already on a team.")) - return - } - - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - res.Send(w, http.StatusOK, request) -} - -// Get a team's pending join requets -// -// @Summary Get team's pending join requests -// @Description Retrieves a team's pending join requests. This is only allowed for the team's owner. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Success 200 {array} sqlc.ListJoinRequestsByTeamAndStatusWithUserRow "Successfully retrieved pending requests" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 403 {object} response.ErrorResponse "Forbidden: Requester is not allowed to perform this action." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /teams/{teamId}/pending-joins [get] -func (h *TeamHandler) GetPendingRequestsForTeam(w http.ResponseWriter, r *http.Request) { - teamId, err := web.PathParamToUUID(r, "teamId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_team_id", "The team ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - requests, err := h.teamService.GetPendingJoinRequestForTeam(r.Context(), *userId, teamId) - if err != nil { - if errors.Is(err, services.ErrUserNotTeamOwner) { - res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You do not have the permissions for this action.")) - return - } - - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - res.Send(w, http.StatusOK, requests) -} - -// Get your pending requests for an event -// -// @Summary Get your pending requests -// @Description Retrieves the current user's pending requests for a specific event's teams. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Success 200 {array} sqlc.TeamJoinRequest "Successfully retrieved pending requests" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// -// @Router /events/{eventId}/teams/me/pending-joins [get] -func (h *TeamHandler) GetMyPendingRequests(w http.ResponseWriter, r *http.Request) { - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_event_id", "The event ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - requests, err := h.teamService.GetUserPendingJoinRequestsByEvent(r.Context(), *userId, eventId) - if err != nil { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) - return - } - - res.Send(w, http.StatusOK, requests) -} - -// Accept/Approve a team join request -// -// @Summary Accept a team join request -// @Description Accepts a pending team join request. Only the team owner can perform this action. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Param request_id path string true "The ID of the join request" -// @Success 204 "Successfully accepted the join request" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 403 {object} response.ErrorResponse "Forbidden: Requester is not allowed to perform this action." -// -// @Failure 404 {object} response.ErrorResponse "Not Found: The join request does not exist." -// @Failure 409 {object} response.ErrorResponse "Conflict: The join request has already been responded to." -// -// @Failure 500 {object} response.ErrorResponse "Something went wrong." -// -// @Router /teams/join/{requestId}/accept [post] -func (h *TeamHandler) AcceptTeamJoinRequest(w http.ResponseWriter, r *http.Request) { - requestId, err := web.PathParamToUUID(r, "requestId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_request_id", "The join request ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - err = h.teamService.RespondToJoinRequest(r.Context(), *userId, requestId, true) - if err != nil { - status, code, message := mapTeamServiceError(err) - res.SendError(w, status, res.NewError(code, message)) - return - } - - res.Send(w, http.StatusNoContent, nil) -} - -// Reject a team join request -// -// @Summary Reject a team join request -// @Description Rejects a pending team join request. Only the team owner can perform this action. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Param request_id path string true "The ID of the join request" -// @Success 204 "Successfully accepted the join request" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 403 {object} response.ErrorResponse "Forbidden: Requester is not allowed to perform this action." -// -// @Failure 404 {object} response.ErrorResponse "Not Found: The join request does not exist." -// @Failure 409 {object} response.ErrorResponse "Conflict: The join request has already been responded to." -// -// @Failure 500 {object} response.ErrorResponse "Something went wrong." -// -// @Router /teams/join/{requestId}/reject [post] -func (h *TeamHandler) RejectTeamJoinRequest(w http.ResponseWriter, r *http.Request) { - requestId, err := web.PathParamToUUID(r, "requestId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_request_id", "The join request ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - err = h.teamService.RespondToJoinRequest(r.Context(), *userId, requestId, false) - if err != nil { - status, code, message := mapTeamServiceError(err) - res.SendError(w, status, res.NewError(code, message)) - return - } - - res.Send(w, http.StatusNoContent, nil) -} - -// Kick a member from a team -// -// @Summary Kick a member from a team -// @Description Kicks a member from a team. Only the team owner can perform this action. -// @Tags Team -// @Param sh_session_id cookie string true "The authenticated session token/id" -// @Param team_id path string true "The ID of the team" -// @Param userId path string true "The ID of the user to be kicked" -// @Success 204 "Successfully kicked the team member" -// @Failure 400 {object} response.ErrorResponse "Bad Request: Missing or malformed parameters." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 403 {object} response.ErrorResponse "Forbidden: Requester is not allowed to perform this action." -// @Failure 500 {object} response.ErrorResponse "Something went wrong." -// -// @Router /teams/{teamId}/members/{userId} [delete] -func (h *TeamHandler) KickMemberFromTeam(w http.ResponseWriter, r *http.Request) { - // Implementation would go here - memberId, err := web.PathParamToUUID(r, "userId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_member_id", "The team member ID is malformed/missing.")) - return - } - - teamId, err := web.PathParamToUUID(r, "teamId") - if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_team_id", "The team ID is malformed/missing.")) - return - } - - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - err = h.teamService.KickMemberFromTeam(r.Context(), memberId, teamId, *userId) - if err != nil { - status, code, message := mapTeamServiceError(err) - res.SendError(w, status, res.NewError(code, message)) - return - } - - res.Send(w, http.StatusNoContent, nil) - -} - -// Maps team service errors to HTTP status codes and messages -func mapTeamServiceError(err error) (status int, code, message string) { - switch { - case errors.Is(err, services.ErrUserNotTeamOwner): - return http.StatusForbidden, "forbidden", "You do not have permission to perform this action." - case errors.Is(err, services.ErrUserNotApplicantOrAttendee): - return http.StatusForbidden, "invalid_user_role", "The user is not an applicant or attendee for this event." - case errors.Is(err, services.ErrUserOnTeam): - return http.StatusConflict, "user_on_team", "The user is already on a team for this event." - case errors.Is(err, services.ErrTeamFull): - return http.StatusConflict, "team_full", "The team is already full." - case errors.Is(err, repository.ErrTeamNotFound): - return http.StatusNotFound, "team_not_found", "Team resource was not found." - case errors.Is(err, services.ErrKickOwnerSelf): - return http.StatusBadRequest, "cannot_kick_owner", "Team owners cannot kick themselves from their own team." - default: - return http.StatusInternalServerError, "internal_error", "Something went wrong." - } -} diff --git a/apps/api/internal/api/handlers/user.go b/apps/api/internal/api/handlers/user.go deleted file mode 100644 index 22be4620..00000000 --- a/apps/api/internal/api/handlers/user.go +++ /dev/null @@ -1,281 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/rs/zerolog" - res "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/ptr" - "github.com/swamphacks/core/apps/api/internal/services" - "github.com/swamphacks/core/apps/api/internal/web" -) - -type UserHandler struct { - userService *services.UserService - logger zerolog.Logger -} - -func NewUserHandler(userService *services.UserService, logger zerolog.Logger) *UserHandler { - return &UserHandler{ - userService: userService, - logger: logger.With().Str("handler", "UserHandler").Str("component", "user").Logger(), - } -} - -// Get User Profile -// -// @Summary Get User Profile -// @Description Get profile information of the currently authenticated user. -// @Tags User -// @Param sh_session cookie string true "The authenticated session token/id" -// @Success 200 {object} sqlc.AuthUser -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 404 {object} response.ErrorResponse "User profile not found." -// @Failure 500 {object} response.ErrorResponse "Something went seriously wrong." -// @Router /users/me [get] -func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - user, err := h.userService.GetUser(r.Context(), *userId) - if err != nil { - h.logger.Err(err).Msg("failed to get user profile") - if err == services.ErrUserNotFound { - res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User profile not found")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went seriously wrong.")) - } - return - } - - res.Send(w, http.StatusOK, user) -} - -type UpdateProfileRequest struct { - Name string `json:"name"` - PreferredEmail string `json:"preferred_email"` -} - -type UpdateEmailConsentRequest struct { - EmailConsent bool `json:"email_consent"` -} - -// Update Email Consent -// -// @Summary Update Email Consent -// @Description Update the user's email consent setting -// @Tags User -// @Param sh_session cookie string true "The authenticated session token/id" -// @Param request body UpdateEmailConsentRequest true "The update email consent request body" -// @Success 200 -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 400 {object} response.ErrorResponse "Invalid request body" -// @Failure 404 {object} response.ErrorResponse "User not found" -// @Failure 500 {object} response.ErrorResponse "Failed to update email consent" -// @Router /users/email-consent [patch] -func (h *UserHandler) UpdateEmailConsent(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - var req UpdateEmailConsentRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) - return - } - - params := sqlc.UpdateUserParams{ - EmailConsentDoUpdate: true, - EmailConsent: req.EmailConsent, - } - - err := h.userService.UpdateUser(r.Context(), *userId, params) - if err != nil { - h.logger.Err(err).Msg("failed to update email consent") - if err == services.ErrUserNotFound { - res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User not found")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("update_failed", "Failed to update email consent")) - } - return - } - - w.WriteHeader(http.StatusOK) -} - -// Update User -// -// @Summary Update User -// @Description Update the user's information -// @Tags User -// @Param sh_session cookie string true "The authenticated session token/id" -// @Param request body UpdateProfileRequest true "The update profile request body" -// @Success 200 -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 400 {object} response.ErrorResponse "Invalid request body" -// @Failure 404 {object} response.ErrorResponse "User not found" -// @Failure 500 {object} response.ErrorResponse "Failed to update user profile" -// @Router /users/me [patch] -func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - var req UpdateProfileRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) - return - } - - // Validate required fields - if req.Name == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Name is required")) - return - } - - // Validate email format - if req.PreferredEmail != "" { - if !email.IsValidEmail(req.PreferredEmail) { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_email", "Invalid email format")) - return - } - } - - params := sqlc.UpdateUserParams{ - ID: *userId, - NameDoUpdate: true, - Name: req.Name, - PreferredEmailDoUpdate: true, - PreferredEmail: &req.PreferredEmail, - } - - err := h.userService.UpdateUser(r.Context(), *userId, params) - if err != nil { - h.logger.Err(err).Msg("failed to update user settings") - if err == services.ErrUserNotFound { - res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User not found")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("update_failed", "Failed to update user settings")) - } - return - } - - w.WriteHeader(http.StatusOK) -} - -type CompleteOnboardingRequest struct { - Name string `json:"name"` - PreferredEmail string `json:"preferred_email"` -} - -// Complete Onboarding -// -// @Summary Complete Onboarding -// @Description Onboard the user. -// @Tags User -// @Param sh_session cookie string true "The authenticated session token/id" -// @Param request body CompleteOnboardingRequest true "The onboarding request body" -// @Success 200 -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 400 {object} response.ErrorResponse "Invalid request body" -// @Failure 404 {object} response.ErrorResponse "User not found" -// @Failure 500 {object} response.ErrorResponse "Failed to complete onboarding" -// @Router /users/me/onboarding [patch] -func (h *UserHandler) CompleteOnboarding(w http.ResponseWriter, r *http.Request) { - userId := ctxutils.GetUserIdFromCtx(r.Context()) - if userId == nil { - res.SendError(w, http.StatusUnauthorized, res.NewError("unauthorized", "User not authenticated")) - return - } - - var req CompleteOnboardingRequest - decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() // Prevents requests with extraneous fields - if err := decoder.Decode(&req); err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) - return - } - - // Validate required fields - if req.Name == "" || req.PreferredEmail == "" { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Name and preferred email are required")) - return - } - - // Validate email format - if !email.IsValidEmail(req.PreferredEmail) { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_email", "Invalid email format")) - return - } - - err := h.userService.CompleteOnboarding(r.Context(), *userId, req.Name, req.PreferredEmail) - if err != nil { - h.logger.Err(err).Msg("failed to complete onboarding") - if err == services.ErrUserNotFound { - res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "User not found")) - } else { - res.SendError(w, http.StatusInternalServerError, res.NewError("update_failed", "Failed to complete onboarding")) - } - return - } - - w.WriteHeader(http.StatusOK) -} - -// Get/Search for users -// -// @Summary Get/Search for users -// @Description Get or search for users by name or email. If no search term is provided, returns all users with pagination. -// @Tags User -// @Param sh_session cookie string true "The authenticated session token/id" -// @Param search query string false "Search term to filter users by name or email (optional)" -// @Param limit query int32 false "Maximum number of users to return (default is 50)" minimum(1) maximum(100) -// @Param offset query int32 false "Number of users to skip for pagination (default is 0)" minimum(0) -// @Success 200 {array} sqlc.AuthUser "OK: Returns a list of users matching the search criteria, or all users if no search term is provided." -// @Failure 401 {object} response.ErrorResponse "Unauthenticated: Requester is not currently authenticated." -// @Failure 400 {object} response.ErrorResponse "Invalid query parameter(s)" -// @Failure 500 {object} response.ErrorResponse "Failed to retrieve users" -// @Router /users [get] -func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) { - queryParams := r.URL.Query() - - // Parse search from query params, and limit, and offset - searchTerm := web.ParseParamString(queryParams, "search", nil) - limit, err := web.ParseParamInt32(queryParams, "limit", ptr.Int32ToPtr(50)) - if err != nil { - h.logger.Err(err).Msg("Limit field was misconfigured. Please check your query parameters.") - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_query", "Your 'limit' query parameter was malformed.")) - return - } - offset, err := web.ParseParamInt32(queryParams, "offset", ptr.Int32ToPtr(0)) - if err != nil { - - h.logger.Err(err).Msg("Offset field was misconfigured. Please check your query parameters.") - res.SendError(w, http.StatusBadRequest, res.NewError("malformed_query", "Your 'offset' query parameter was malformed.")) - return - } - - users, err := h.userService.GetAllUsers(r.Context(), searchTerm, *limit, *offset) - if err != nil { - h.logger.Err(err).Msg("Failed to retrieve all users") - res.SendError(w, http.StatusInternalServerError, res.NewError("update_failed", "Failed to retrieve users")) - return - } - - res.Send(w, http.StatusOK, users) -} diff --git a/apps/api/internal/api/middleware/auth.go b/apps/api/internal/api/middleware/auth.go index fbb0e392..1135eab5 100644 --- a/apps/api/internal/api/middleware/auth.go +++ b/apps/api/internal/api/middleware/auth.go @@ -9,13 +9,16 @@ import ( "strings" "time" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/google/uuid" "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" "github.com/swamphacks/core/apps/api/internal/api/response" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/cookie" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type ctxKey string @@ -23,11 +26,13 @@ type ctxKey string // Use this variable to retrieve the user object later from context! const UserContextKey ctxKey = "user" const SessionContextKey ctxKey = "session" +const UserRoleContextKey ctxKey = "eventRole" type AuthMiddleware struct { - db *db.DB - logger zerolog.Logger - cfg *config.Config + db *database.DB + logger zerolog.Logger + cfg *config.Config + userRepo *repository.UserRepository } // UserContext represents the authenticated user in API requests. @@ -49,27 +54,84 @@ type UserContext struct { Onboarded bool `json:"onboarded" example:"true"` // Optional profile image URL - Image *string `json:"image,omitempty" example:"https://cdn.example.com/avatar.png" extensions:"nullable"` + Image *string `json:"image" example:"https://cdn.example.com/avatar.png"` // Role assigned to the user - Role sqlc.AuthUserRole `json:"role"` + Role sqlc.UserRole `json:"role" enum:"admin,staff,attendee,applicant,visitor"` // Whether the user agreed to receive emails EmailConsent bool `json:"emailConsent" example:"false"` + + Rfid *string `json:"rfid"` + + CheckedInAt *time.Time `json:"checkedInAt"` } type SessionContext struct { SessionID uuid.UUID } -func NewAuthMiddleware(db *db.DB, logger zerolog.Logger, cfg *config.Config) *AuthMiddleware { +func NewAuthMiddleware(userRepo *repository.UserRepository, db *database.DB, logger zerolog.Logger, cfg *config.Config) *AuthMiddleware { return &AuthMiddleware{ - db: db, - logger: logger.With().Str("middleware", "AuthMiddleware").Str("component", "api").Logger(), - cfg: cfg, + db: db, + logger: logger.With().Str("middleware", "AuthMiddleware").Logger(), + cfg: cfg, + userRepo: userRepo, } } +type RawWriterKey struct{} +type RawRequestKey struct{} + +func (m *AuthMiddleware) RawHTTPMiddlewareHuma(ctx huma.Context, next func(huma.Context)) { + r, w := humachi.Unwrap(ctx) + + newCtx := context.WithValue(ctx.Context(), RawWriterKey{}, w) + newCtx = context.WithValue(newCtx, RawRequestKey{}, r) + + next(huma.WithContext(ctx, newCtx)) +} + +// TODO: remove this extra layer and use RequireAuth directly +func (m *AuthMiddleware) RequireAuthHuma(ctx huma.Context, next func(huma.Context)) { + r, w := humachi.Unwrap(ctx) + + m.RequireAuth(http.HandlerFunc(func(_ http.ResponseWriter, newR *http.Request) { + next(huma.WithContext(ctx, newR.Context())) + })).ServeHTTP(w, r.WithContext(ctx.Context())) +} + +// TODO: remove this extra layer and use RequireRole directly +func (m *AuthMiddleware) RequireRoleHuma(roles []sqlc.UserRole) func(ctx huma.Context, next func(huma.Context)) { + return func(ctx huma.Context, next func(huma.Context)) { + r, w := humachi.Unwrap(ctx) + + m.RequireRoles(roles)(http.HandlerFunc(func(_ http.ResponseWriter, newR *http.Request) { + next(huma.WithContext(ctx, newR.Context())) + })).ServeHTTP(w, r.WithContext(ctx.Context())) + } +} + +func (m *AuthMiddleware) RequireAdminHuma(ctx huma.Context, next func(huma.Context)) { + r, w := humachi.Unwrap(ctx) + + mwHandler := m.RequireRoles([]sqlc.UserRole{sqlc.UserRoleAdmin}) + + mwHandler(http.HandlerFunc(func(_ http.ResponseWriter, newR *http.Request) { + next(huma.WithContext(ctx, newR.Context())) + })).ServeHTTP(w, r.WithContext(ctx.Context())) +} + +func (m *AuthMiddleware) RequireStaffHuma(ctx huma.Context, next func(huma.Context)) { + r, w := humachi.Unwrap(ctx) + + mwHandler := m.RequireRoles([]sqlc.UserRole{sqlc.UserRoleAdmin, sqlc.UserRoleStaff}) + + mwHandler(http.HandlerFunc(func(_ http.ResponseWriter, newR *http.Request) { + next(huma.WithContext(ctx, newR.Context())) + })).ServeHTTP(w, r.WithContext(ctx.Context())) +} + func (m *AuthMiddleware) RequireMobileAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m.logger.Trace().Msg("Incoming mobile request") @@ -101,7 +163,7 @@ func (m *AuthMiddleware) RequireMobileAuth(next http.Handler) http.Handler { func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m.logger.Trace().Msg("Checking auth status") - cookie, err := r.Cookie("sh_session_id") + cookie, err := r.Cookie(cookie.SessionCookieName) if err != nil { m.logger.Warn().Msg("Missing session cookie.") response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized")) @@ -127,6 +189,7 @@ func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler { return } + // TODO: I don't think we need UserContext here, just return sqlc.User directly userContext := UserContext{ UserID: user.UserID, Name: user.Name, @@ -136,6 +199,8 @@ func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler { Onboarded: user.Onboarded, Role: user.Role, EmailConsent: user.EmailConsent, + Rfid: user.Rfid, + CheckedInAt: user.CheckedInAt, } sessionContext := SessionContext{ @@ -146,11 +211,12 @@ func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler { ctx := context.WithValue(r.Context(), UserContextKey, &userContext) ctx = context.WithValue(ctx, SessionContextKey, &sessionContext) + next.ServeHTTP(w, r.WithContext(ctx)) }) } -func (m *AuthMiddleware) RequirePlatformRole(roles []sqlc.AuthUserRole) func(http.Handler) http.Handler { +func (m *AuthMiddleware) RequireRoles(roles []sqlc.UserRole) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // get user from context @@ -161,14 +227,13 @@ func (m *AuthMiddleware) RequirePlatformRole(roles []sqlc.AuthUserRole) func(htt return } - if userCtx.Role == sqlc.AuthUserRoleSuperuser { + if userCtx.Role == sqlc.UserRoleAdmin { next.ServeHTTP(w, r) return } - // check if user role matches required role if !slices.Contains(roles, userCtx.Role) { - m.logger.Warn().Msgf("User tried to access %s with insufficient permissions as role %s", r.URL.Path, string(userCtx.Role)) + m.logger.Warn().Msgf("User tried to access %s with insufficient permissions (eventRole: %s)", r.URL.Path, string(userCtx.Role)) response.SendError(w, http.StatusForbidden, response.NewError("forbidden", "You are forbidden from this resource.")) return } diff --git a/apps/api/internal/api/middleware/events.go b/apps/api/internal/api/middleware/events.go deleted file mode 100644 index b8dd5333..00000000 --- a/apps/api/internal/api/middleware/events.go +++ /dev/null @@ -1,109 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "slices" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/api/response" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/web" -) - -const EventContextKey ctxKey = "event" - -type EventMiddleware struct { - db *db.DB - logger zerolog.Logger - cfg *config.Config - eventRespository *repository.EventRepository -} - -type EventContext struct { - EventRole *sqlc.EventRole -} - -func NewEventMiddleware(db *db.DB, logger zerolog.Logger, cfg *config.Config) *EventMiddleware { - return &EventMiddleware{ - db: db, - logger: logger.With().Str("middleware", "EventMiddleware").Str("component", "api").Logger(), - cfg: cfg, - eventRespository: repository.NewEventRespository(db), - } -} - -func (m *EventMiddleware) AttachEventRoleToContext() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userCtx, ok := r.Context().Value(UserContextKey).(*UserContext) - if !ok { - m.logger.Warn().Msg("No event role context found.") - response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized.")) - return - } - - eventId, err := web.PathParamToUUID(r, "eventId") - if err != nil { - response.SendError(w, http.StatusBadRequest, response.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - eventRole, err := m.eventRespository.GetEventRoleByIds(r.Context(), userCtx.UserID, eventId) - - eventRoleContext := EventContext{ - EventRole: eventRole, - } - - ctx := context.WithValue(r.Context(), EventContextKey, &eventRoleContext) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -func (m *EventMiddleware) RequireEventRole(eventRoles []sqlc.EventRoleType) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // get user from context - userCtx, ok := r.Context().Value(UserContextKey).(*UserContext) - if !ok { - m.logger.Warn().Msg("No event role context found.") - response.SendError(w, http.StatusUnauthorized, response.NewError("no_auth", "You are not authorized.")) - return - } - - eventIdStr := chi.URLParam(r, "eventId") - eventId, err := uuid.Parse(eventIdStr) - if err != nil { - response.SendError(w, http.StatusBadRequest, response.NewError("invalid_event_id", "The event ID is not a valid UUID")) - return - } - - if userCtx.Role == sqlc.AuthUserRoleSuperuser { - next.ServeHTTP(w, r) - return - } - - userEventRole, err := m.eventRespository.GetEventRoleByIds(r.Context(), userCtx.UserID, eventId) - if err != nil { - // TODO: Will throw if user doesn't have permission, but how should we handle that with other possible errors? - m.logger.Warn().Msgf("Error while trying to access %s with insufficient permissions (userId: %s, eventId: %s)", r.URL.Path, userCtx.UserID, eventId) - response.SendError(w, http.StatusForbidden, response.NewError("forbidden", "You are forbidden from this resource.")) - return - - } - if !slices.Contains(eventRoles, userEventRole.Role) { - m.logger.Warn().Msgf("User tried to access %s with insufficient permissions (eventRole: %s)", r.URL.Path, string(userEventRole.Role)) - response.SendError(w, http.StatusForbidden, response.NewError("forbidden", "You are forbidden from this resource.")) - return - } - - next.ServeHTTP(w, r) - }) - } -} diff --git a/apps/api/internal/api/middleware/middleware.go b/apps/api/internal/api/middleware/middleware.go index 26c51b19..72fae369 100644 --- a/apps/api/internal/api/middleware/middleware.go +++ b/apps/api/internal/api/middleware/middleware.go @@ -3,17 +3,16 @@ package middleware import ( "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" ) type Middleware struct { - Auth *AuthMiddleware - Event *EventMiddleware + Auth *AuthMiddleware } -func NewMiddleware(db *db.DB, logger zerolog.Logger, cfg *config.Config) *Middleware { +func NewMiddleware(userRepo *repository.UserRepository, db *database.DB, logger zerolog.Logger, config *config.Config) *Middleware { return &Middleware{ - Auth: NewAuthMiddleware(db, logger, cfg), - Event: NewEventMiddleware(db, logger, cfg), + Auth: NewAuthMiddleware(userRepo, db, logger, config), } } diff --git a/apps/api/internal/web/path.go b/apps/api/internal/api/web/path.go similarity index 100% rename from apps/api/internal/web/path.go rename to apps/api/internal/api/web/path.go diff --git a/apps/api/internal/web/query.go b/apps/api/internal/api/web/query.go similarity index 100% rename from apps/api/internal/web/query.go rename to apps/api/internal/api/web/query.go diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 1387331e..6d79d9a5 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -10,8 +10,9 @@ import ( ) type CookieConfig struct { - Domain string `env:"DOMAIN"` - Secure bool `env:"SECURE"` + SessionName string `env:"SESSION_NAME"` + Domain string `env:"DOMAIN"` + Secure bool `env:"SECURE"` } type OAuthConfig struct { @@ -47,16 +48,17 @@ type AWSConfig struct { } type CoreBuckets struct { - Avatars string `env:"USER_AVATARS" envDefault:"core-user-avatars-dev"` - QRCodes string `env:"USER_QRCODES" envDefault:"core-user-qrcodes-dev"` - ApplicationResumes string `env:"APPLICATION_RESUMES" envDefault:"core-application-resumes-dev"` - EventAssets string `env:"EVENT_ASSETS" envDefault:"core-event-assets-dev"` + Avatars string `env:"USER_AVATARS" envDefault:"xii-core-user-avatars-dev"` + QRCodes string `env:"USER_QRCODES" envDefault:"xii-core-user-qrcodes-dev"` + ApplicationResumes string `env:"APPLICATION_RESUMES" envDefault:"xii-core-application-resumes-dev"` + EventAssets string `env:"EVENT_ASSETS" envDefault:"xii-core-event-assets-dev"` AvatarsBaseUrl string `env:"USER_AVATARS_BASE_URL"` QRCodesBaseUrl string `env:"USER_QRCODES_BASE_URL"` EventAssetsBaseUrl string `env:"EVENT_ASSETS_BASE_URL"` } type Config struct { + AppEnv string `env:"APP_ENV"` DatabaseURL string `env:"DATABASE_URL"` RedisURL string `env:"REDIS_URL"` Port string `env:"PORT" envDefault:"8080"` @@ -79,7 +81,7 @@ type Config struct { MobileAuthKey string `env:"MOBILE_AUTH_KEY"` } -func Load() *Config { +func LoadConfig() *Config { loadEnv() cfg, err := env.ParseAs[Config]() diff --git a/apps/api/internal/cookie/cookie.go b/apps/api/internal/cookie/cookie.go deleted file mode 100644 index 61b940fd..00000000 --- a/apps/api/internal/cookie/cookie.go +++ /dev/null @@ -1,48 +0,0 @@ -package cookie - -import ( - "net/http" - "time" - - "github.com/google/uuid" - "github.com/swamphacks/core/apps/api/internal/config" -) - -func SetSessionCookie(w http.ResponseWriter, sessionID uuid.UUID, expiresAt time.Time, cfg config.CookieConfig) { - http.SetCookie(w, &http.Cookie{ - Name: "sh_session_id", - Value: sessionID.String(), - Domain: cfg.Domain, - Path: "/", - HttpOnly: true, - Secure: cfg.Secure, - SameSite: http.SameSiteLaxMode, - Expires: expiresAt, - }) -} - -func ClearSessionCookie(w http.ResponseWriter, cfg config.CookieConfig) { - http.SetCookie(w, &http.Cookie{ - Name: "sh_session_id", - Value: "", - Domain: cfg.Domain, - Path: "/", - HttpOnly: true, - Secure: cfg.Secure, - SameSite: http.SameSiteLaxMode, - Expires: time.Unix(0, 0), - MaxAge: -1, - }) -} - -func ExpireCookie(w http.ResponseWriter, cfg config.CookieConfig, name string) { - http.SetCookie(w, &http.Cookie{ - Name: name, - Value: "", - Domain: cfg.Domain, - Path: "/", - SameSite: http.SameSiteLaxMode, - Expires: time.Unix(0, 0), - MaxAge: -1, - }) -} diff --git a/apps/api/internal/ctxutils/event.go b/apps/api/internal/ctxutils/event.go deleted file mode 100644 index 1e8bf1bf..00000000 --- a/apps/api/internal/ctxutils/event.go +++ /dev/null @@ -1,18 +0,0 @@ -package ctxutils - -import ( - "context" - - mw "github.com/swamphacks/core/apps/api/internal/api/middleware" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -// Takes in the request context and returns the eventRole or nil if not retrievable -func GetEventRoleFromCtx(ctx context.Context) *sqlc.EventRole { - eventCtx, ok := ctx.Value(mw.EventContextKey).(*mw.EventContext) - if !ok { - return nil - } - - return eventCtx.EventRole -} diff --git a/apps/api/internal/ctxutils/user.go b/apps/api/internal/ctxutils/user.go index d5e86ca8..be14ef8c 100644 --- a/apps/api/internal/ctxutils/user.go +++ b/apps/api/internal/ctxutils/user.go @@ -3,37 +3,15 @@ package ctxutils import ( "context" - "github.com/google/uuid" mw "github.com/swamphacks/core/apps/api/internal/api/middleware" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" ) -// Takes in a context and returns whether the user has the superuser role -func IsSuperuser(ctx context.Context) bool { +func GetUserFromCtx(ctx context.Context) *mw.UserContext { userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) - if !ok { - return false - } - - return userCtx.Role == sqlc.AuthUserRoleSuperuser -} -// Takes in a context and returns whether the user has the user role -func IsUser(ctx context.Context) bool { - userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) - if !ok { - return false - } - - return userCtx.Role == sqlc.AuthUserRoleUser -} - -// Takes in the request context and returns the userID or nil if not retrievable -func GetUserIdFromCtx(ctx context.Context) *uuid.UUID { - userCtx, ok := ctx.Value(mw.UserContextKey).(*mw.UserContext) if !ok { return nil } - return &userCtx.UserID + return userCtx } diff --git a/apps/api/internal/db/connection.go b/apps/api/internal/database/database.go similarity index 82% rename from apps/api/internal/db/connection.go rename to apps/api/internal/database/database.go index 33c06d0d..afd8ccaf 100644 --- a/apps/api/internal/db/connection.go +++ b/apps/api/internal/database/database.go @@ -1,11 +1,11 @@ -package db +package database import ( "context" "log" "github.com/jackc/pgx/v5/pgxpool" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type DB struct { diff --git a/apps/api/internal/database/errors.go b/apps/api/internal/database/errors.go new file mode 100644 index 00000000..b55ef189 --- /dev/null +++ b/apps/api/internal/database/errors.go @@ -0,0 +1,47 @@ +package database + +import ( + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// Errors +var ( + ErrEntityNotFound = errors.New("Entity not found") + ErrUnexpectedFileType = errors.New("did not expect this file type") + ErrFailedToUploadBanner = errors.New("failed to upload banner") + ErrFailedToUpdateHackathon = errors.New("failed to update hackathon") + + // Account + ErrAccountNotFound = errors.New("account not found") + + // Application + ErrCreateApplication = errors.New("unable to create application") + ErrSaveApplication = errors.New("unable to save application") + ErrSubmitApplication = errors.New("unable to submit application") + ErrInvalidApplicationData = errors.New("unable to parse application data") + ErrGetApplication = errors.New("unable to get application for user") + ErrApplicationNotFound = errors.New("can not find application for user") + + // Bat runs + ErrDuplicateRun = errors.New("Run already exists in the database") + ErrRunNotFound = errors.New("Run not found") + ErrNoRunsDeleted = errors.New("No Runs deleted") + ErrMultipleRunsDeleted = errors.New("Multiple Runs affected by delete query expecting to delete one") + + // Emails + ErrDuplicateEmails = errors.New("email already exists in the database") +) + +func IsUniqueViolation(err error) bool { + if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok { + return pgErr.Code == "23505" + } + return false +} + +func IsNotFound(err error) bool { + return errors.Is(err, pgx.ErrNoRows) +} diff --git a/apps/api/internal/database/migrations/20260326160116_database_init.sql b/apps/api/internal/database/migrations/20260326160116_database_init.sql new file mode 100644 index 00000000..cabf4c47 --- /dev/null +++ b/apps/api/internal/database/migrations/20260326160116_database_init.sql @@ -0,0 +1,338 @@ +-- +goose Up +-- +goose StatementBegin + +-- TYPES +create type application_status as enum ('started', 'submitted', 'under_review', 'accepted', 'rejected', 'waitlisted', 'withdrawn'); + +create type team_invitation_status as enum ('pending', 'accepted', 'expired', 'rejected'); + +create type team_join_request_status as enum ('pending', 'approved', 'rejected'); + +create type bat_run_status as enum ('running', 'completed', 'failed'); + +create type user_role as enum ('admin', 'staff', 'attendee', 'applicant', 'visitor'); + +-- TABLES +create table users +( + id uuid default gen_random_uuid() not null primary key, + name text not null, + email text unique, + email_verified boolean default false not null, + onboarded boolean default false not null, + image text, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + preferred_email text, + email_consent boolean default false not null, + checked_in_at timestamptz, + rfid text, + role_assigned_at timestamptz, + role user_role default 'visitor'::user_role not null +); + +create table accounts +( + id uuid default gen_random_uuid() not null primary key, + user_id uuid not null references users on delete cascade, + provider_id text not null, + account_id text not null, + hashed_password text, + access_token text, + refresh_token text, + id_token text, + access_token_expires_at timestamptz, + refresh_token_expires_at timestamptz, + scope text, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + unique (provider_id, account_id) +); + +create table sessions +( + id uuid default gen_random_uuid() not null primary key, + user_id uuid not null references users on delete cascade, + expires_at timestamptz not null, + ip_address text, + user_agent text, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + last_used_at timestamptz default now() not null +); + +create table hackathons +( + id text not null primary key, + name text not null, + description text, + location text, + location_url text, + max_attendees integer, + application_open timestamptz not null, + application_close timestamptz not null, + rsvp_deadline timestamptz, + decision_release timestamptz, + start_time timestamptz not null, + end_time timestamptz not null, + is_active boolean default false not null, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + banner text, + application_review_started boolean default false not null +); + +create table applications +( + user_id uuid not null primary key references users on delete cascade, + status application_status default 'started'::application_status not null, + application jsonb default '{}'::jsonb not null, + created_at timestamptz default now() not null, + saved_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + submitted_at timestamptz, + experience_rating integer, + passion_rating integer, + assigned_reviewer_id uuid references users on delete set null, + waitlist_join_time timestamptz, + hackathon_id text not null references hackathons(id) +); + +create table bat_runs +( + id uuid default gen_random_uuid() not null primary key, + accepted_applicants uuid[] default '{}'::uuid[], + rejected_applicants uuid[] default '{}'::uuid[], + status bat_run_status default 'running'::bat_run_status not null, + created_at timestamptz default now() not null, + completed_at timestamptz, + hackathon_id text not null references hackathons(id) +); + +create table interest_submissions +( + id uuid default gen_random_uuid() not null primary key, + email text unique not null, + created_at timestamptz default now() not null, + source text, + hackathon_id text not null references hackathons(id) +); + +create table redeemables +( + id uuid default gen_random_uuid() not null primary key, + name varchar(255) not null, + amount integer not null constraint redeemables_amount_check check (amount >= 0), + max_user_amount integer not null constraint redeemables_max_user_amount_check check (max_user_amount >= 1), + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + hackathon_id text not null references hackathons(id) +); + +create table teams +( + id uuid default gen_random_uuid() not null primary key, + name text not null, + owner_id uuid references users on delete set null, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + hackathon_id text not null references hackathons(id) +); + +create table team_invitations +( + id uuid default gen_random_uuid() not null primary key, + team_id uuid not null references teams on delete cascade, + invited_by_user_id uuid not null references users on delete cascade, + invited_email text not null, + invited_user_id uuid references users on delete cascade, + status team_invitation_status default 'pending'::team_invitation_status not null, + expires_at timestamptz, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null +); + +create table team_join_requests +( + id uuid default gen_random_uuid() not null primary key, + team_id uuid not null references teams on delete cascade, + user_id uuid not null references users on delete cascade, + request_message text, + status team_join_request_status default 'pending'::team_join_request_status not null, + processed_by_user_id uuid references users on delete set null, + processed_at timestamptz, + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null +); + +create table team_members +( + user_id uuid not null references users on delete cascade, + team_id uuid not null references teams on delete cascade, + joined_at timestamptz default now() not null, + primary key (user_id, team_id) +); + +create table user_redemptions +( + user_id uuid not null references users on delete cascade, + redeemable_id uuid not null references redeemables on delete cascade, + amount integer not null constraint user_redemptions_amount_check check (amount >= 0), + created_at timestamptz default now() not null, + updated_at timestamptz default now() not null, + hackathon_id text not null references hackathons(id), + primary key (user_id, redeemable_id) +); + +-- INDEXES +create unique index only_one_hackathon_active on hackathons (is_active) where is_active = true; + +create unique index idx_unique_pending_request + on team_join_requests (team_id, user_id) + where (status = 'pending'::team_join_request_status); + +create index idx_applications_status + on applications (status); + +create index idx_accounts_user_id + on accounts (user_id); + +create index idx_accounts_provider_account + on accounts (provider_id, account_id); + +create index idx_sessions_user_id + on sessions (user_id); + +create index idx_sessions_expires_at + on sessions (expires_at); + +-- TRIGGERS +create or replace function update_modified_column() +returns TRIGGER as $$ +begin + NEW.updated_at = clock_timestamp(); + return NEW; +end; +$$ language plpgsql; + +create trigger set_updated_at_accounts + before update + on accounts + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_users + before update + on users + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_teams + before update + on users + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_sessions + before update + on sessions + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_hackathon + before update + on hackathons + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_applications + before update + on applications + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_redeemables + before update + on redeemables + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_team_invitations + before update + on team_invitations + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_team_join_requests + before update + on team_join_requests + for each row + execute procedure update_modified_column(); + +create trigger set_updated_at_user_redemptions + before update + on user_redemptions + for each row + execute procedure update_modified_column(); + +-- +goose StatementEnd +-- +goose Down +drop index if exists only_one_hackathon_active; + +drop index if exists idx_unique_pending_request; + +drop index if exists idx_applications_status; + +drop index if exists idx_accounts_user_id; + +drop index if exists idx_accounts_provider_account; + +drop index if exists idx_sessions_user_id; + +drop index if exists idx_sessions_expires_at; + +drop trigger if exists set_updated_at_accounts on accounts; + +drop trigger if exists set_updated_at_users on users; + +drop trigger if exists set_updated_at_teams on teams; + +drop trigger if exists set_updated_at_sessions on sessions; + +drop trigger if exists set_updated_at_hackathon on hackathon; + +drop trigger if exists set_updated_at_applications on applications; + +drop trigger if exists set_updated_at_redeemables on redeemables; + +drop trigger if exists set_updated_at_team_invitations on invitations; + +drop trigger if exists set_updated_at_team_join_requests on team_join_requests; + +drop trigger if exists set_updated_at_user_redemptions on user_redemptions; + +drop table user_redemptions; +drop table team_members; +drop table team_join_requests; +drop table team_invitations; +drop table teams; +drop table redeemables; +drop table interest_submissions; +drop table bat_runs; +drop table applications; +drop table hackathons; +drop table sessions; +drop table accounts; +drop table users; + +drop type application_status; + +drop type team_invitation_status; + +drop type team_join_request_status; + +drop type bat_run_status; + +drop type user_role; + +drop function update_modified_column; \ No newline at end of file diff --git a/apps/api/internal/db/queries/accounts.sql b/apps/api/internal/database/queries/accounts.sql similarity index 84% rename from apps/api/internal/db/queries/accounts.sql rename to apps/api/internal/database/queries/accounts.sql index 14cd34bf..3d609d76 100644 --- a/apps/api/internal/db/queries/accounts.sql +++ b/apps/api/internal/database/queries/accounts.sql @@ -1,5 +1,5 @@ -- name: CreateAccount :one -INSERT INTO auth.accounts ( +INSERT INTO accounts ( user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope @@ -11,15 +11,15 @@ INSERT INTO auth.accounts ( RETURNING *; -- name: GetByProviderAndAccountID :one -SELECT * FROM auth.accounts +SELECT * FROM accounts WHERE provider_id = $1 AND account_id = $2; -- name: GetByUserID :many -SELECT * FROM auth.accounts +SELECT * FROM accounts WHERE user_id = $1; -- name: UpdateTokens :exec -UPDATE auth.accounts +UPDATE accounts SET access_token = $3, refresh_token = $4, id_token = $5, @@ -29,10 +29,10 @@ SET access_token = $3, WHERE provider_id = $1 AND account_id = $2; -- name: DeleteAccount :exec -DELETE FROM auth.accounts +DELETE FROM accounts WHERE provider_id = $1 AND account_id = $2; -- name: GetUserIDByDiscordAccountID :one SELECT user_id -FROM auth.accounts +FROM accounts WHERE provider_id = 'discord' AND account_id = $1; diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/database/queries/applications.sql similarity index 63% rename from apps/api/internal/db/queries/applications.sql rename to apps/api/internal/database/queries/applications.sql index 4e70e8c5..bec81d07 100644 --- a/apps/api/internal/db/queries/applications.sql +++ b/apps/api/internal/database/queries/applications.sql @@ -1,14 +1,8 @@ -- name: CreateApplication :one -INSERT INTO applications ( - user_id, event_id -) VALUES ( - $1, $2 -) -RETURNING *; +INSERT INTO applications (user_id, hackathon_id) VALUES ($1, $2) RETURNING *; --- name: GetApplicationByUserAndEventID :one -SELECT * FROM applications -WHERE user_id = $1 AND event_id = $2; +-- name: GetApplicationByUserId :one +SELECT * FROM applications WHERE user_id = $1; -- name: UpdateApplication :exec UPDATE applications @@ -21,25 +15,23 @@ SET experience_rating = CASE WHEN @experience_rating_do_update::boolean THEN @experience_rating::INT ELSE experience_rating END, passion_rating = CASE WHEN @passion_rating_do_update::boolean THEN @passion_rating::INT ELSE passion_rating END WHERE - user_id = @user_id AND event_id = @event_id; + user_id = @user_id; -- name: DeleteApplication :exec -DELETE FROM applications -WHERE user_id = $1 AND event_id = $2; +DELETE FROM applications WHERE user_id = $1; --- An application is considered "available" for an event if the application has a status of submitted and has not been reviewed yet. +-- An application is considered "available" if the application has a status of submitted and has not been reviewed yet. -- For optimization purposes, we only select the application IDs. --- name: ListAvailableApplicationsForEvent :many +-- name: ListAvailableApplications :many SELECT user_id FROM applications -WHERE event_id = $1 - AND status = 'submitted' +WHERE status = 'submitted' AND experience_rating IS NULL AND passion_rating IS NULL ORDER BY user_id ASC; --- name: ListAdmissionCandidatesByEvent :many +-- name: ListAdmissionCandidates :many SELECT a.user_id, a.passion_rating, a.experience_rating, @@ -50,19 +42,15 @@ LEFT JOIN team_members tm ON tm.user_id = a.user_id LEFT JOIN teams t ON t.id = tm.team_id - AND t.event_id = a.event_id -WHERE a.event_id = $1 - AND a.status = 'under_review' +WHERE a.status = 'under_review' AND a.passion_rating IS NOT NULL AND a.experience_rating IS NOT NULL; - -- name: AssignApplicationsToReviewer :exec UPDATE applications SET assigned_reviewer_id = @reviewer_id::uuid, status = 'under_review' -WHERE user_id = ANY(@application_ids::uuid[]) - AND event_id = @event_id::uuid; +WHERE user_id = ANY(@application_ids::uuid[]); -- name: ResetApplicationReviews :exec UPDATE applications @@ -70,55 +58,48 @@ SET assigned_reviewer_id = NULL, status = 'submitted', experience_rating = NULL, passion_rating = NULL -WHERE status NOT IN ('submitted', 'started') - AND event_id = $1; +WHERE status NOT IN ('submitted', 'started'); --- name: ListApplicationByReviewerAndEvent :many +-- name: ListApplicationByReviewer :many SELECT user_id, passion_rating, experience_rating FROM applications WHERE assigned_reviewer_id = $1 - AND event_id = $2 AND status IN ('under_review') ORDER BY user_id ASC; --- name: ListNonReviewedApplicationsByEvent :many +-- name: ListNonReviewedApplications :many SELECT user_id FROM applications -WHERE event_id = $1 - AND status = 'under_review' +WHERE status = 'under_review' AND (passion_rating IS NULL OR experience_rating IS NULL); -- name: JoinWaitlist :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' -WHERE user_id = $1 AND event_id = $2; +WHERE user_id = $1; --- name: UpdateApplicationStatusByEventID :exec +-- name: UpdateApplicationStatus :exec UPDATE applications SET status = @status::application_status -WHERE event_id = @event_id::uuid - AND user_id = ANY(@user_ids::uuid[]); +WHERE user_id = ANY(@user_ids::uuid[]); --- name: TransitionAcceptedApplicationsToWaitlistByEventID :exec +-- name: TransitionAcceptedApplicationsToWaitlist :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' -WHERE event_id = @event_id::uuid - AND status = 'accepted' +WHERE status = 'accepted' AND user_id IN ( - SELECT user_id from event_roles AS er - WHERE er.role = 'applicant' -) -; + SELECT id from users + WHERE role = 'applicant' +); --- name: TransitionWaitlistedApplicationsToAcceptedByEventID :many +-- name: TransitionWaitlistedApplicationsToAccepted :many UPDATE applications SET waitlist_join_time = NULL, status = 'accepted' WHERE user_id IN ( SELECT user_id FROM applications - WHERE event_id = @event_id::uuid - AND status = 'waitlisted' + WHERE status = 'waitlisted' ORDER BY waitlist_join_time ASC LIMIT @acceptanceCount::int ) diff --git a/apps/api/internal/db/queries/bat_runs.sql b/apps/api/internal/database/queries/bat_runs.sql similarity index 65% rename from apps/api/internal/db/queries/bat_runs.sql rename to apps/api/internal/database/queries/bat_runs.sql index 1e2dd105..adcb1b4e 100644 --- a/apps/api/internal/db/queries/bat_runs.sql +++ b/apps/api/internal/database/queries/bat_runs.sql @@ -1,28 +1,18 @@ --- name: AddRun :one -INSERT INTO bat_runs ( - event_id -) VALUES ( - $1 -) RETURNING *; +-- name: AddBatRun :one +INSERT INTO bat_runs (hackathon_id) VALUES ($1) RETURNING *; --- name: GetRunById :one +-- name: GetBatRunById :one SELECT * FROM bat_runs WHERE id = $1; --- name: GetRunsByEventId :many +-- name: GetBatRuns :many SELECT - id, - accepted_applicants, - rejected_applicants, - status, - created_at, - completed_at + * FROM bat_runs -WHERE event_id = $1 ORDER BY created_at DESC; --- name: UpdateRunById :exec +-- name: UpdateBatRunById :exec UPDATE bat_runs SET accepted_applicants = CASE WHEN @accepted_applicants_do_update::boolean THEN @accepted_applicants ELSE accepted_applicants END, @@ -33,6 +23,6 @@ WHERE id = @id::uuid RETURNING *; --- name: DeleteRunById :execrows +-- name: DeleteBatRunById :execrows DELETE FROM bat_runs WHERE id = $1; diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/database/queries/hackathons.sql similarity index 53% rename from apps/api/internal/db/queries/events.sql rename to apps/api/internal/database/queries/hackathons.sql index a12d2981..927f0fb2 100644 --- a/apps/api/internal/db/queries/events.sql +++ b/apps/api/internal/database/queries/hackathons.sql @@ -1,14 +1,13 @@ --- name: CreateEvent :one -INSERT INTO events ( - name, +-- name: CreateHackathon :one +INSERT INTO hackathons ( + id, name, application_open, application_close, start_time, end_time, description, location, location_url, max_attendees, - rsvp_deadline, decision_release, - website_url, is_published + rsvp_deadline, decision_release, is_active ) VALUES ( -- FIXME: The second parameter in coalesce MUST be the default value created in the schema. I have not found a more automated way to insert the default value. - @name, + @id, @name, @application_open, @application_close, @start_time, @end_time, coalesce(sqlc.narg(description), NULL), @@ -17,17 +16,12 @@ INSERT INTO events ( coalesce(sqlc.narg(max_attendees), NULL::INT), coalesce(sqlc.narg(rsvp_deadline), NULL::TIMESTAMPTZ), coalesce(sqlc.narg(decision_release), NULL::TIMESTAMPTZ), - coalesce(sqlc.narg(website_url), NULL), - coalesce(sqlc.narg(is_published), FALSE) + coalesce(sqlc.narg(is_active), false) ) RETURNING *; --- name: GetEventByID :one -SELECT * FROM events -WHERE id = $1; - --- name: UpdateEventById :exec -UPDATE events +-- name: UpdateHackathon :exec +UPDATE hackathons SET name = CASE WHEN @name_do_update::boolean THEN @name ELSE name END, description = CASE WHEN @description_do_update::boolean THEN @description ELSE description END, @@ -40,63 +34,33 @@ SET decision_release = CASE WHEN @decision_release_do_update::boolean THEN @decision_release ELSE decision_release END, start_time = CASE WHEN @start_time_do_update::boolean THEN @start_time ELSE start_time END, end_time = CASE WHEN @end_time_do_update::boolean THEN @end_time ELSE end_time END, - website_url = CASE WHEN @website_url_do_update::boolean THEN @website_url ELSE website_url END, - is_published = CASE WHEN @is_published_do_update::boolean THEN @is_published ELSE is_published END, banner = CASE WHEN @banner_do_update::boolean THEN @banner ELSE banner END, application_review_started = CASE WHEN @application_review_started_do_update::boolean THEN @application_review_started ELSE application_review_started END -WHERE - id = @id::uuid +WHERE is_active = true RETURNING *; --- name: DeleteEventById :execrows --- execrows returns affect row count on top of an error -DELETE FROM events -WHERE id = $1; +-- name: GetHackathon :one +SELECT * FROM hackathons WHERE is_active = true; --- name: GetEventRoleByIds :one -SELECT * FROM event_roles -WHERE user_id = @user_id::uuid AND event_id = @event_id::uuid; +-- name: GetStaff :many +SELECT * FROM users +WHERE role IN ('admin', 'staff'); --- name: GetPublishedEvents :many -SELECT - e.*, - er.role AS event_role -FROM events e -LEFT JOIN event_roles AS er - ON er.event_id = e.id - AND er.user_id = $1 -WHERE e.is_published = TRUE -ORDER BY e.start_time ASC; +-- name: GetAttendeesWithDiscord :many +SELECT + a.account_id as discord_id, + u.id as user_id, + u.name, + u.email +FROM users u +JOIN accounts a ON u.id = a.user_id +WHERE u.role = 'attendee' + AND a.provider_id = 'discord'; --- name: GetAllEvents :many -SELECT - e.*, - er.role AS event_role -FROM events e -LEFT JOIN event_roles AS er - ON er.event_id = e.id - AND er.user_id = $1 -ORDER BY e.start_time ASC; +-- name: GetAttendeeCount :one +SELECT COUNT(*) FROM users +WHERE role = 'attendee'; --- name: GetEventsWithUserInfo :many -SELECT - e.*, - er.role AS event_role, - a.status AS application_status -FROM events e -LEFT JOIN event_roles er - ON er.event_id = e.id - AND er.user_id = sqlc.narg(user_id) -LEFT JOIN applications a - ON a.event_id = e.id - AND a.user_id = sqlc.narg(user_id) -WHERE - CASE sqlc.arg(scope)::get_event_scope_type - WHEN 'all' THEN - TRUE - WHEN 'scoped' THEN - e.is_published = TRUE OR (e.is_published = FALSE AND (er.role = 'staff' or er.role = 'admin')) - ELSE - e.is_published = TRUE - END -ORDER BY e.start_time ASC; +-- name: GetAttendeeUserIds :many +SELECT id FROM users +WHERE role = 'attendee'; \ No newline at end of file diff --git a/apps/api/internal/database/queries/interest_submissions.sql b/apps/api/internal/database/queries/interest_submissions.sql new file mode 100644 index 00000000..57573f94 --- /dev/null +++ b/apps/api/internal/database/queries/interest_submissions.sql @@ -0,0 +1,12 @@ +-- name: AddEmail :one +-- Adds a new email to the mailing list for a specific user. +-- The unique constraint on `email` will prevent duplicates. +-- Returns the newly created email record. +INSERT INTO interest_submissions ( + email, + source, + hackathon_id +) VALUES ( + $1, $2, $3 +) +RETURNING *; \ No newline at end of file diff --git a/apps/api/internal/db/queries/redeemables.sql b/apps/api/internal/database/queries/redeemables.sql similarity index 61% rename from apps/api/internal/db/queries/redeemables.sql rename to apps/api/internal/database/queries/redeemables.sql index 9619b52b..c94fe522 100644 --- a/apps/api/internal/db/queries/redeemables.sql +++ b/apps/api/internal/database/queries/redeemables.sql @@ -1,7 +1,5 @@ --- name: GetRedeemablesByEventID :many --- Using event id, return all redeemables associated. Should also return data like how many have been redeemed already -SELECT r.id, -r.event_id, +-- name: GetRedeemables :many +SELECT r.id, r.name, r.amount AS total_stock, r.max_user_amount, @@ -10,35 +8,31 @@ r.updated_at, COALESCE(SUM(ur.amount), 0) AS total_redeemed FROM redeemables r LEFT JOIN user_redemptions ur ON r.id = ur.redeemable_id -WHERE r.event_id = $1 GROUP BY r.id; -- name: RedeemRedeemable :one --- Using user id and redeemable id, attempt! to redeem a redeemable -INSERT INTO user_redemptions (user_id, redeemable_id, amount) -SELECT $1, $2, 1 +INSERT INTO user_redemptions (user_id, redeemable_id, hackathon_id, amount) +SELECT @user_id, @redeemable_id, 1 WHERE ( SELECT COALESCE(SUM(amount), 0) FROM user_redemptions - WHERE redeemable_id = $2 -) < (SELECT amount FROM redeemables WHERE id = $2) + WHERE redeemable_id = @redeemable_id +) < (SELECT amount FROM redeemables WHERE id = @redeemable_id) ON CONFLICT (user_id, redeemable_id) DO UPDATE SET amount = user_redemptions.amount + 1, updated_at = CURRENT_TIMESTAMP -WHERE user_redemptions.amount < (SELECT max_user_amount FROM redeemables WHERE id = $2) +WHERE user_redemptions.amount < (SELECT max_user_amount FROM redeemables WHERE id = @redeemable_id) RETURNING *; -- name: GetRedemptionInfoByRedeemableID :many --- Gather all redemption info for a specific reedeemable (who has redeemed already) SELECT ur.user_id, ur.redeemable_id, ur.amount, ur.created_at, ur.updated_at FROM user_redemptions ur WHERE ur.redeemable_id = $1; -- name: CreateRedeemable :one --- Create a new redeemable for an event -INSERT INTO redeemables (event_id, name, amount, max_user_amount) -VALUES ($1, $2, $3, $4) +INSERT INTO redeemables (name, amount, max_user_amount, hackathon_id) +VALUES (@name, @amount, @max_user_amount, @hackthon_id) RETURNING *; -- name: UpdateRedeemable :one @@ -52,12 +46,10 @@ WHERE id = $1 RETURNING *; -- name: DeleteRedeemable :exec --- Delete a redeemable by id DELETE FROM redeemables WHERE id = $1; -- name: UpdateRedemption :exec --- Update a redemption record for a user and redeemable (for removing redemption mostly) UPDATE user_redemptions SET amount = $1 diff --git a/apps/api/internal/db/queries/sessions.sql b/apps/api/internal/database/queries/sessions.sql similarity index 65% rename from apps/api/internal/db/queries/sessions.sql rename to apps/api/internal/database/queries/sessions.sql index 788f40a8..a0fe6132 100644 --- a/apps/api/internal/db/queries/sessions.sql +++ b/apps/api/internal/database/queries/sessions.sql @@ -1,39 +1,39 @@ -- name: CreateSession :one -INSERT INTO auth.sessions (user_id, expires_at, ip_address, user_agent) +INSERT INTO sessions (user_id, expires_at, ip_address, user_agent) VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetSessionByID :one -SELECT * FROM auth.sessions +SELECT * FROM sessions WHERE id = $1; -- name: GetSessionsByUserID :many -SELECT * FROM auth.sessions +SELECT * FROM sessions WHERE user_id = $1; -- name: UpdateSessionExpiration :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = $2 WHERE id = $1; -- name: DeleteExpiredSession :exec -DELETE FROM auth.sessions +DELETE FROM sessions WHERE expires_at < NOW(); -- name: GetActiveSessionUserInfo :one -SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, s.last_used_at -FROM auth.sessions s -JOIN auth.users u ON s.user_id = u.id +SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, u.checked_in_at, u.rfid, s.last_used_at +FROM sessions s +JOIN users u ON s.user_id = u.id WHERE s.id = $1 AND (s.expires_at > NOW()) LIMIT 1; -- name: TouchSession :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = $2, last_used_at = NOW() WHERE id = $1; -- name: InvalidateSessionByID :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = NOW() WHERE id = $1; diff --git a/apps/api/internal/db/queries/stats.sql b/apps/api/internal/database/queries/stats.sql similarity index 91% rename from apps/api/internal/db/queries/stats.sql rename to apps/api/internal/database/queries/stats.sql index 7a0cfef2..8f1475ec 100644 --- a/apps/api/internal/db/queries/stats.sql +++ b/apps/api/internal/database/queries/stats.sql @@ -7,8 +7,7 @@ SELECT COUNT(*) FILTER (WHERE application->>'gender' = 'non-binary') AS non_binary, COUNT(*) FILTER (WHERE application->>'gender' = '') AS other FROM applications -WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1; +WHERE status <> 'started' AND status IS NOT NULL; -- name: GetApplicationAgeSplit :one SELECT @@ -20,8 +19,7 @@ SELECT COUNT(*) FILTER (WHERE (application->>'age')::int = 22) AS age_22, COUNT(*) FILTER (WHERE (application->>'age')::int >= 23) AS age_23_plus FROM applications -WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1; +WHERE status <> 'started' AND status IS NOT NULL; -- name: GetApplicationRaceSplit :many SELECT @@ -33,7 +31,6 @@ SELECT COUNT(*) AS count FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY CASE WHEN application->>'race' IS NOT NULL AND application->>'race' <> '' THEN application->>'race' @@ -48,7 +45,6 @@ SELECT COUNT(*) AS count FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY (application->>'school')::text ORDER BY count DESC; @@ -59,7 +55,6 @@ SELECT FROM applications, LATERAL unnest(string_to_array(application->>'majors', ',')) AS major WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY trim(major) ORDER BY count DESC; @@ -72,14 +67,13 @@ SELECT COUNT(*) FILTER (WHERE status = 'rejected') AS rejected, COUNT(*) FILTER (WHERE status = 'waitlisted') AS waitlisted, COUNT(*) FILTER (WHERE status = 'withdrawn') AS withdrawn -FROM applications -WHERE event_id = $1; +FROM applications; -- name: GetSubmissionTimes :many SELECT date_trunc('day', submitted_at AT TIME ZONE 'US/Eastern')::date AS day, COUNT(*) AS count FROM applications -WHERE event_id = $1 AND submitted_at IS NOT NULL +WHERE submitted_at IS NOT NULL GROUP BY day ORDER By day; diff --git a/apps/api/internal/db/queries/team_invitations.sql b/apps/api/internal/database/queries/team_invitations.sql similarity index 100% rename from apps/api/internal/db/queries/team_invitations.sql rename to apps/api/internal/database/queries/team_invitations.sql diff --git a/apps/api/internal/db/queries/team_join_requests.sql b/apps/api/internal/database/queries/team_join_requests.sql similarity index 90% rename from apps/api/internal/db/queries/team_join_requests.sql rename to apps/api/internal/database/queries/team_join_requests.sql index 9e97eed2..b56549e6 100644 --- a/apps/api/internal/db/queries/team_join_requests.sql +++ b/apps/api/internal/database/queries/team_join_requests.sql @@ -38,7 +38,7 @@ RETURNING *; DELETE FROM team_join_requests WHERE id = @id; --- name: ListTeamJoinRequestsByUserAndEventAndStatus :many +-- name: ListTeamJoinRequestsByUserAndStatus :many SELECT tjr.* FROM team_join_requests tjr WHERE tjr.user_id = $1 @@ -47,11 +47,10 @@ WHERE tjr.user_id = $1 SELECT 1 FROM teams t WHERE t.id = tjr.team_id - AND t.event_id = $3 ) ORDER BY tjr.created_at DESC; --- name: DeleteJoinRequestsByUserAndEventAndStatus :exec +-- name: DeleteJoinRequestsByUserAndStatus :exec DELETE FROM team_join_requests tjr WHERE tjr.user_id = $1 AND tjr.status = $2 @@ -59,7 +58,6 @@ WHERE tjr.user_id = $1 SELECT 1 FROM teams t WHERE t.id = tjr.team_id - AND t.event_id = $3 ); -- name: ListJoinRequestsByTeamAndStatusWithUser :many @@ -69,7 +67,7 @@ SELECT u.name AS user_name, u.image AS user_image FROM team_join_requests tjr -JOIN auth.users u ON u.id = tjr.user_id +JOIN users u ON u.id = tjr.user_id WHERE tjr.team_id = @team_id::uuid AND tjr.status = @status::join_request_status ORDER BY tjr.created_at DESC; \ No newline at end of file diff --git a/apps/api/internal/db/queries/team_members.sql b/apps/api/internal/database/queries/team_members.sql similarity index 85% rename from apps/api/internal/db/queries/team_members.sql rename to apps/api/internal/database/queries/team_members.sql index fd6c742c..50f813f1 100644 --- a/apps/api/internal/db/queries/team_members.sql +++ b/apps/api/internal/database/queries/team_members.sql @@ -23,16 +23,15 @@ SELECT FROM team_members tm JOIN - auth.users u ON tm.user_id = u.id + users u ON tm.user_id = u.id WHERE tm.team_id = $1; --- name: GetTeamMemberByUserAndEvent :one +-- name: GetTeamMemberByUserId :one SELECT tm.* FROM team_members tm JOIN teams t on tm.team_id = t.id WHERE tm.user_id = $1 - AND t.event_id = $2 LIMIT 1; -- name: CreateTeamMember :one diff --git a/apps/api/internal/db/queries/teams.sql b/apps/api/internal/database/queries/teams.sql similarity index 81% rename from apps/api/internal/db/queries/teams.sql rename to apps/api/internal/database/queries/teams.sql index 1632d8da..353f646f 100644 --- a/apps/api/internal/db/queries/teams.sql +++ b/apps/api/internal/database/queries/teams.sql @@ -2,7 +2,7 @@ INSERT INTO teams ( name, owner_id, - event_id + hackathon_id ) VALUES ( $1, $2, @@ -14,19 +14,16 @@ RETURNING *; DELETE FROM teams WHERE id = $1; --- name: GetUserEventTeam :one +-- name: GetUserTeam :one SELECT t.id, t.name, - t.owner_id, - t.event_id + t.owner_id FROM teams t JOIN team_members tm ON t.id = tm.team_id -WHERE - t.event_id = $1 - AND tm.user_id = $2 +WHERE tm.user_id = $1 LIMIT 1; -- name: GetTeamById :one @@ -43,12 +40,11 @@ WHERE id = @id::uuid RETURNING *; --- name: ListTeamsWithMembersByEvent :many +-- name: ListTeamsWithMembers :many SELECT t.id, t.name, t.owner_id, - t.event_id, -- Step 1: Cast the aggregated JSON array to JSONB (COALESCE( json_agg( @@ -67,12 +63,10 @@ FROM LEFT JOIN team_members tm ON t.id = tm.team_id LEFT JOIN - auth.users u ON tm.user_id = u.id -WHERE - t.event_id = $1 + users u ON tm.user_id = u.id GROUP BY t.id ORDER BY t.created_at DESC -LIMIT $2 -OFFSET $3; \ No newline at end of file +LIMIT $1 +OFFSET $2; \ No newline at end of file diff --git a/apps/api/internal/db/queries/users.sql b/apps/api/internal/database/queries/users.sql similarity index 60% rename from apps/api/internal/db/queries/users.sql rename to apps/api/internal/database/queries/users.sql index cd9c7337..924d9fea 100644 --- a/apps/api/internal/db/queries/users.sql +++ b/apps/api/internal/database/queries/users.sql @@ -1,14 +1,14 @@ -- name: CreateUser :one -INSERT INTO auth.users (name, email, image) +INSERT INTO users (name, email, image) VALUES ($1, $2, $3) RETURNING *; -- name: GetUserByID :one -SELECT * FROM auth.users +SELECT * FROM users WHERE id = $1; -- name: GetUserByEmail :one -SELECT * FROM auth.users +SELECT * FROM users WHERE email = $1; -- name: GetUserEmailInfoById :one @@ -20,16 +20,16 @@ SELECT WHEN preferred_email IS NOT NULL AND preferred_email != '' THEN preferred_email ELSE email END AS contact_email -FROM auth.users +FROM users WHERE id = $1; -- name: UpdateUserOnboarded :exec -UPDATE auth.users +UPDATE users SET onboarded = TRUE WHERE id = $1; -- name: UpdateUser :exec -UPDATE auth.users +UPDATE users SET name = CASE WHEN @name_do_update::boolean THEN @name ELSE name END, email = CASE WHEN @email_do_update::boolean THEN @email ELSE email END, @@ -38,18 +38,48 @@ SET onboarded = CASE WHEN @onboarded_do_update::boolean THEN @onboarded ELSE onboarded END, image = CASE WHEN @image_do_update::boolean THEN @image ELSE image END, email_consent = CASE WHEN @email_consent_do_update::boolean THEN @email_consent ELSE email_consent END, + checked_in_at = CASE WHEN @checked_in_at_do_update::boolean THEN @checked_in_at ELSE checked_in_at END, + rfid = CASE WHEN @rfid_do_update::boolean THEN @rfid ELSE rfid END, + role = CASE WHEN @role_do_update::boolean THEN @role ELSE role END, + role_assigned_at = CASE WHEN @role_do_update::boolean THEN NOW() ELSE role_assigned_at END, updated_at = NOW() WHERE id = @id::uuid; -- name: DeleteUser :exec -DELETE FROM auth.users +DELETE FROM users WHERE id = $1; -- name: GetUsers :many SELECT * -FROM auth.users +FROM users WHERE LOWER(name) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%') OR LOWER(email) LIKE LOWER('%' || COALESCE(sqlc.arg('search'), '') || '%') ORDER BY name LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset'); + +-- name: GetUserByRFID :one +SELECT * FROM users +WHERE rfid = $1; + +-- name: UpdateRole :exec +UPDATE users +SET role = @role::role_type, + role_assigned_at = NOW() +WHERE id = @user_id::uuid; + +-- name: RemoveRole :exec +UPDATE users +SET role = NULL, + role_assigned_at = NOW() +WHERE id = @user_id::uuid; + +-- name: UpdateCheckInTime :exec +UPDATE users +SET checked_in_at = @checked_in_at +WHERE id = @user_id::uuid; + +-- name: UpdateRFID :exec +UPDATE users +SET rfid = @rfid +WHERE id = @user_id::uuid; \ No newline at end of file diff --git a/apps/api/internal/db/repository/accounts.go b/apps/api/internal/database/repository/accounts.go similarity index 63% rename from apps/api/internal/db/repository/accounts.go rename to apps/api/internal/database/repository/accounts.go index 2a5a9fbb..ca406102 100644 --- a/apps/api/internal/db/repository/accounts.go +++ b/apps/api/internal/database/repository/accounts.go @@ -6,26 +6,22 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrAccountNotFound = errors.New("account not found") + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type AccountRepository struct { - db *db.DB + db *database.DB } -func NewAccountRespository(db *db.DB) *AccountRepository { +func NewAccountRespository(db *database.DB) *AccountRepository { return &AccountRepository{ db: db, } } func (r *AccountRepository) NewTx(tx pgx.Tx) *AccountRepository { - txDB := &db.DB{ + txDB := &database.DB{ Pool: r.db.Pool, Query: sqlc.New(tx), } @@ -35,23 +31,38 @@ func (r *AccountRepository) NewTx(tx pgx.Tx) *AccountRepository { } } -func (r *AccountRepository) Create(ctx context.Context, params sqlc.CreateAccountParams) (*sqlc.AuthAccount, error) { +func (r *AccountRepository) Create(ctx context.Context, params sqlc.CreateAccountParams) (*sqlc.Account, error) { account, err := r.db.Query.CreateAccount(ctx, params) - return &account, err + + if err != nil { + return nil, err + } + + return &account, nil } -func (r *AccountRepository) GetByProviderAndAccountID(ctx context.Context, params sqlc.GetByProviderAndAccountIDParams) (*sqlc.AuthAccount, error) { +func (r *AccountRepository) GetByProviderAndAccountID(ctx context.Context, params sqlc.GetByProviderAndAccountIDParams) (*sqlc.Account, error) { account, err := r.db.Query.GetByProviderAndAccountID(ctx, params) - return &account, err + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, database.ErrAccountNotFound + } + return nil, err + } + + return &account, nil } func (r *AccountRepository) GetUserIDByDiscordAccountID(ctx context.Context, discordAccountID string) (*uuid.UUID, error) { userID, err := r.db.Query.GetUserIDByDiscordAccountID(ctx, discordAccountID) + if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrAccountNotFound + return nil, database.ErrAccountNotFound } return nil, err } + return &userID, nil -} \ No newline at end of file +} diff --git a/apps/api/internal/database/repository/application.go b/apps/api/internal/database/repository/application.go new file mode 100644 index 00000000..472c7942 --- /dev/null +++ b/apps/api/internal/database/repository/application.go @@ -0,0 +1,137 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type ApplicationRepository struct { + db *database.DB +} + +func NewApplicationRepository(db *database.DB) *ApplicationRepository { + return &ApplicationRepository{ + db: db, + } +} + +func (r *ApplicationRepository) NewTx(tx pgx.Tx) *ApplicationRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &ApplicationRepository{ + db: txDB, + } +} + +func (r *ApplicationRepository) CreateApplication(ctx context.Context, params sqlc.CreateApplicationParams) (*sqlc.Application, error) { + application, err := r.db.Query.CreateApplication(ctx, params) + + if err != nil { + return nil, err + } + + return &application, nil +} + +func (r *ApplicationRepository) UpdateApplication(ctx context.Context, params sqlc.UpdateApplicationParams) error { + return r.db.Query.UpdateApplication(ctx, params) +} + +func (r *ApplicationRepository) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { + application, err := r.db.Query.GetApplicationByUserId(ctx, userID) + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, database.ErrApplicationNotFound + } + + return nil, err + } + + return &application, nil +} + +func (r *ApplicationRepository) UpdateApplicationsStatuses(ctx context.Context, params sqlc.UpdateApplicationStatusParams) error { + return r.db.Query.UpdateApplicationStatus(ctx, params) +} + +// List all candidates considered for admission. +// This queries for all applications who are 'under_review' and have their rating fields filled out. +// It also LEFT JOINs in their team id (if they have one) for further grouping based on teams. +func (r *ApplicationRepository) ListAdmissionCandidates(ctx context.Context) ([]sqlc.ListAdmissionCandidatesRow, error) { + return r.db.Query.ListAdmissionCandidates(ctx) +} + +func (r *ApplicationRepository) ListAvailableApplications(ctx context.Context) ([]uuid.UUID, error) { + return r.db.Query.ListAvailableApplications(ctx) +} + +func (r *ApplicationRepository) AssignApplicationToReview(ctx context.Context, params sqlc.AssignApplicationsToReviewerParams) error { + return r.db.Query.AssignApplicationsToReviewer(ctx, params) +} + +func (r *ApplicationRepository) ListApplicationByReviewer(ctx context.Context, reviewerID uuid.UUID) ([]sqlc.ListApplicationByReviewerRow, error) { + return r.db.Query.ListApplicationByReviewer(ctx, &reviewerID) +} + +func (r *ApplicationRepository) ResetApplicationReviews(ctx context.Context) error { + return r.db.Query.ResetApplicationReviews(ctx) +} + +func (r *ApplicationRepository) GetSubmittedApplicationGenders(ctx context.Context) (sqlc.GetApplicationGenderSplitRow, error) { + return r.db.Query.GetApplicationGenderSplit(ctx) +} + +func (r *ApplicationRepository) GetSubmittedApplicationRaces(ctx context.Context) ([]sqlc.GetApplicationRaceSplitRow, error) { + return r.db.Query.GetApplicationRaceSplit(ctx) +} + +func (r *ApplicationRepository) GetSubmittedApplicationAges(ctx context.Context) (sqlc.GetApplicationAgeSplitRow, error) { + return r.db.Query.GetApplicationAgeSplit(ctx) +} + +func (r *ApplicationRepository) GetSubmittedApplicationMajors(ctx context.Context) ([]sqlc.GetApplicationMajorSplitRow, error) { + return r.db.Query.GetApplicationMajorSplit(ctx) +} + +func (r *ApplicationRepository) GetSubmittedApplicationSchools(ctx context.Context) ([]sqlc.GetApplicationSchoolSplitRow, error) { + return r.db.Query.GetApplicationSchoolSplit(ctx) +} + +func (r *ApplicationRepository) GetApplicationStatuses(ctx context.Context) (sqlc.GetApplicationStatusSplitRow, error) { + return r.db.Query.GetApplicationStatusSplit(ctx) +} + +func (r *ApplicationRepository) GetNonReviewedApplications(ctx context.Context) ([]uuid.UUID, error) { + return r.db.Query.ListNonReviewedApplications(ctx) +} + +func (r *ApplicationRepository) GetSubmissionTimes(ctx context.Context) ([]sqlc.GetSubmissionTimesRow, error) { + return r.db.Query.GetSubmissionTimes(ctx) +} + +func (r *ApplicationRepository) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { + return r.db.Query.JoinWaitlist(ctx, userID) +} + +func (r *ApplicationRepository) TransitionAcceptedApplicationsToWaitlist(ctx context.Context) error { + return r.db.Query.TransitionAcceptedApplicationsToWaitlist(ctx) +} + +func (r *ApplicationRepository) TransitionWaitlistedApplicationsToAccepted(ctx context.Context, acceptanceCount int32) ([]uuid.UUID, error) { + return r.db.Query.TransitionWaitlistedApplicationsToAccepted(ctx, acceptanceCount) +} + +func (r *ApplicationRepository) GetAttendeeCount(ctx context.Context) (uint32, error) { + amount, err := r.db.Query.GetAttendeeCount(ctx) + + return uint32(amount), err +} diff --git a/apps/api/internal/database/repository/bat_runs.go b/apps/api/internal/database/repository/bat_runs.go new file mode 100644 index 00000000..dc74a851 --- /dev/null +++ b/apps/api/internal/database/repository/bat_runs.go @@ -0,0 +1,67 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type BatRunsRepository struct { + db *database.DB +} + +func NewBatRunsRepository(db *database.DB) *BatRunsRepository { + return &BatRunsRepository{ + db: db, + } +} + +func (r *BatRunsRepository) AddRun(ctx context.Context, hackathonID string) (*sqlc.BatRun, error) { + run, err := r.db.Query.AddBatRun(ctx, hackathonID) + if err != nil { + if database.IsUniqueViolation(err) { + return nil, database.ErrDuplicateRun + } + return nil, err + } + return &run, nil +} + +func (r *BatRunsRepository) GetRunById(ctx context.Context, id uuid.UUID) (sqlc.BatRun, error) { + return r.db.Query.GetBatRunById(ctx, id) +} + +func (r *BatRunsRepository) GetRuns(ctx context.Context) (*[]sqlc.BatRun, error) { + runs, err := r.db.Query.GetBatRuns(ctx) + return &runs, err +} + +func (r *BatRunsRepository) UpdateRunById(ctx context.Context, params sqlc.UpdateBatRunByIdParams) error { + err := r.db.Query.UpdateBatRunById(ctx, params) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return database.ErrRunNotFound + } + } + return err +} + +func (r *BatRunsRepository) DeleteRunById(ctx context.Context, id uuid.UUID) error { + affectedRows, err := r.db.Query.DeleteBatRunById(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return database.ErrRunNotFound + } + } + if affectedRows == 0 { + return database.ErrNoRunsDeleted + } else if affectedRows > 1 { + return database.ErrMultipleRunsDeleted + } + + return err +} diff --git a/apps/api/internal/database/repository/event_interests.go b/apps/api/internal/database/repository/event_interests.go new file mode 100644 index 00000000..db021820 --- /dev/null +++ b/apps/api/internal/database/repository/event_interests.go @@ -0,0 +1,31 @@ +package repository + +import ( + "context" + + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type EventInterestsRepository struct { + db *database.DB +} + +func NewEventInterestsRepository(db *database.DB) *EventInterestsRepository { + return &EventInterestsRepository{ + db: db, + } +} + +func (r *EventInterestsRepository) AddEmail(ctx context.Context, params sqlc.AddEmailParams) (*sqlc.InterestSubmission, error) { + interestSubmission, err := r.db.Query.AddEmail(ctx, params) + if err != nil { + if database.IsUniqueViolation(err) { + return nil, database.ErrDuplicateEmails + } + + return nil, err + } + + return &interestSubmission, nil +} diff --git a/apps/api/internal/database/repository/hackathon.go b/apps/api/internal/database/repository/hackathon.go new file mode 100644 index 00000000..9296153e --- /dev/null +++ b/apps/api/internal/database/repository/hackathon.go @@ -0,0 +1,72 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type HackathonRepository struct { + db *database.DB +} + +func NewHackathonRepository(db *database.DB) *HackathonRepository { + return &HackathonRepository{ + db: db, + } +} + +func (r *HackathonRepository) NewTx(tx pgx.Tx) *HackathonRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &HackathonRepository{ + db: txDB, + } +} + +func (r *HackathonRepository) CreateHackathon(ctx context.Context, params sqlc.CreateHackathonParams) (*sqlc.Hackathon, error) { + hackathon, err := r.db.Query.CreateHackathon(ctx, params) + return &hackathon, err +} + +func (r *HackathonRepository) GetHackathon(ctx context.Context) (*sqlc.Hackathon, error) { + hackathon, err := r.db.Query.GetHackathon(ctx) + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, database.ErrEntityNotFound + } + return nil, err + } + + return &hackathon, err +} + +func (r *HackathonRepository) UpdateHackathon(ctx context.Context, params sqlc.UpdateHackathonParams) error { + return r.db.Query.UpdateHackathon(ctx, params) +} + +func (r *HackathonRepository) GetStaff(ctx context.Context) (*[]sqlc.User, error) { + users, err := r.db.Query.GetStaff(ctx) + return &users, err +} + +func (r *HackathonRepository) GetAttendeesWithDiscord(ctx context.Context) (*[]sqlc.GetAttendeesWithDiscordRow, error) { + attendees, err := r.db.Query.GetAttendeesWithDiscord(ctx) + return &attendees, err +} + +func (r *HackathonRepository) GetAttendeeUserIds(ctx context.Context) ([]uuid.UUID, error) { + return r.db.Query.GetAttendeeUserIds(ctx) +} + +func (r *HackathonRepository) GetAttendeeCount(ctx context.Context) (int64, error) { + return r.db.Query.GetAttendeeCount(ctx) +} diff --git a/apps/api/internal/database/repository/redeemables.go b/apps/api/internal/database/repository/redeemables.go new file mode 100644 index 00000000..31bdc2ce --- /dev/null +++ b/apps/api/internal/database/repository/redeemables.go @@ -0,0 +1,67 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type RedeemablesRepository struct { + db *database.DB +} + +func NewRedeemablesRepository(db *database.DB) *RedeemablesRepository { + return &RedeemablesRepository{ + db: db, + } +} + +func (r *RedeemablesRepository) GetRedeemables(ctx context.Context) (*[]sqlc.GetRedeemablesRow, error) { + redeemables, err := r.db.Query.GetRedeemables(ctx) + if err != nil { + return nil, err + } + return &redeemables, nil +} + +func (r *RedeemablesRepository) CreateRedeemable(ctx context.Context, params sqlc.CreateRedeemableParams) (*sqlc.Redeemable, error) { + redeemable, err := r.db.Query.CreateRedeemable(ctx, params) + if err != nil { + return nil, err + } + return &redeemable, nil +} + +func (r *RedeemablesRepository) DeleteRedeemable(ctx context.Context, redeemableID uuid.UUID) error { + err := r.db.Query.DeleteRedeemable(ctx, redeemableID) + if err != nil { + return err + } + return nil +} + +func (r *RedeemablesRepository) UpdateRedeemable(ctx context.Context, params sqlc.UpdateRedeemableParams) (*sqlc.Redeemable, error) { + redeemable, err := r.db.Query.UpdateRedeemable(ctx, params) + if err != nil { + return nil, err + } + return &redeemable, nil +} + +func (r *RedeemablesRepository) RedeemRedeemable(ctx context.Context, params sqlc.RedeemRedeemableParams) (*sqlc.UserRedemption, error) { + redemption, err := r.db.Query.RedeemRedeemable(ctx, params) + if err != nil { + return nil, err + } + return &redemption, nil +} + +func (r *RedeemablesRepository) UpdateRedemption(ctx context.Context, params sqlc.UpdateRedemptionParams) error { + err := r.db.Query.UpdateRedemption(ctx, params) + if err != nil { + return err + } + return nil +} diff --git a/apps/api/internal/db/repository/sessions.go b/apps/api/internal/database/repository/sessions.go similarity index 60% rename from apps/api/internal/db/repository/sessions.go rename to apps/api/internal/database/repository/sessions.go index c1804765..5bed17f8 100644 --- a/apps/api/internal/db/repository/sessions.go +++ b/apps/api/internal/database/repository/sessions.go @@ -5,22 +5,22 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type SessionRepository struct { - db *db.DB + db *database.DB } -func NewSessionRepository(db *db.DB) *SessionRepository { +func NewSessionRepository(db *database.DB) *SessionRepository { return &SessionRepository{ db: db, } } func (r *SessionRepository) NewTx(tx pgx.Tx) *SessionRepository { - txDB := &db.DB{ + txDB := &database.DB{ Pool: r.db.Pool, Query: sqlc.New(tx), } @@ -30,11 +30,11 @@ func (r *SessionRepository) NewTx(tx pgx.Tx) *SessionRepository { } } -func (r *SessionRepository) Create(ctx context.Context, params sqlc.CreateSessionParams) (*sqlc.AuthSession, error) { +func (r *SessionRepository) Create(ctx context.Context, params sqlc.CreateSessionParams) (*sqlc.Session, error) { session, err := r.db.Query.CreateSession(ctx, params) return &session, err } -func (r *SessionRepository) Invalidate(ctx context.Context, sessionId uuid.UUID) error { - return r.db.Query.InvalidateSessionByID(ctx, sessionId) +func (r *SessionRepository) Invalidate(ctx context.Context, sessionID uuid.UUID) error { + return r.db.Query.InvalidateSessionByID(ctx, sessionID) } diff --git a/apps/api/internal/db/repository/team_invitations.go b/apps/api/internal/database/repository/team_invitations.go similarity index 58% rename from apps/api/internal/db/repository/team_invitations.go rename to apps/api/internal/database/repository/team_invitations.go index cb333685..70dfbcb5 100644 --- a/apps/api/internal/db/repository/team_invitations.go +++ b/apps/api/internal/database/repository/team_invitations.go @@ -2,22 +2,22 @@ package repository import ( "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type TeamInvitationRepository struct { - db *db.DB + db *database.DB } -func NewTeamInvitationRespository(db *db.DB) *TeamInvitationRepository { +func NewTeamInvitationRespository(db *database.DB) *TeamInvitationRepository { return &TeamInvitationRepository{ db: db, } } func (r *TeamInvitationRepository) NewTx(tx pgx.Tx) *TeamInvitationRepository { - txDB := &db.DB{ + txDB := &database.DB{ Pool: r.db.Pool, Query: sqlc.New(tx), } diff --git a/apps/api/internal/database/repository/team_join_requests.go b/apps/api/internal/database/repository/team_join_requests.go new file mode 100644 index 00000000..704f16c4 --- /dev/null +++ b/apps/api/internal/database/repository/team_join_requests.go @@ -0,0 +1,80 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +type TeamJoinRequestRepository struct { + db *database.DB +} + +func NewTeamJoinRequestRepository(db *database.DB) *TeamJoinRequestRepository { + return &TeamJoinRequestRepository{ + db: db, + } +} + +func (r *TeamJoinRequestRepository) NewTx(tx pgx.Tx) *TeamJoinRequestRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &TeamJoinRequestRepository{ + db: txDB, + } +} + +func (r *TeamJoinRequestRepository) Create(ctx context.Context, params sqlc.CreateTeamJoinRequestParams) (*sqlc.TeamJoinRequest, error) { + request, err := r.db.Query.CreateTeamJoinRequest(ctx, params) + + if err != nil { + return nil, err + } + + return &request, nil +} + +func (r *TeamJoinRequestRepository) GetById(ctx context.Context, requestID uuid.UUID) (*sqlc.TeamJoinRequest, error) { + request, err := r.db.Query.GetTeamJoinRequestByID(ctx, requestID) + if err != nil { + return nil, err + } + + return &request, nil +} + +func (r *TeamJoinRequestRepository) ListJoinRequestsByUser(ctx context.Context, userID uuid.UUID) ([]sqlc.TeamJoinRequest, error) { + return r.db.Query.ListTeamJoinRequestsByUserID(ctx, userID) +} + +func (r *TeamJoinRequestRepository) ListJoinRequestsByTeam(ctx context.Context, params sqlc.ListTeamJoinRequestsByTeamIDAndStatusParams) ([]sqlc.TeamJoinRequest, error) { + return r.db.Query.ListTeamJoinRequestsByTeamIDAndStatus(ctx, params) +} + +func (r *TeamJoinRequestRepository) ListJoinRequestsByTeamWithUser(ctx context.Context, params sqlc.ListJoinRequestsByTeamAndStatusWithUserParams) ([]sqlc.ListJoinRequestsByTeamAndStatusWithUserRow, error) { + return r.db.Query.ListJoinRequestsByTeamAndStatusWithUser(ctx, params) +} + +func (r *TeamJoinRequestRepository) ListJoinRequestsByUserAndStatus(ctx context.Context, params sqlc.ListTeamJoinRequestsByUserAndStatusParams) ([]sqlc.TeamJoinRequest, error) { + return r.db.Query.ListTeamJoinRequestsByUserAndStatus(ctx, params) +} + +func (r *TeamJoinRequestRepository) DeleteByUserAndStatus(ctx context.Context, params sqlc.DeleteJoinRequestsByUserAndStatusParams) error { + return r.db.Query.DeleteJoinRequestsByUserAndStatus(ctx, params) +} + +func (r *TeamJoinRequestRepository) UpdateStatus(ctx context.Context, params sqlc.UpdateTeamJoinRequestParams) (*sqlc.TeamJoinRequest, error) { + request, err := r.db.Query.UpdateTeamJoinRequest(ctx, params) + + if err != nil { + return nil, err + } + + return &request, nil +} diff --git a/apps/api/internal/database/repository/team_members.go b/apps/api/internal/database/repository/team_members.go new file mode 100644 index 00000000..3185e177 --- /dev/null +++ b/apps/api/internal/database/repository/team_members.go @@ -0,0 +1,58 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrTeamMemberNotFound = errors.New("team member not found") +) + +type TeamMemberRepository struct { + db *database.DB +} + +func NewTeamMemberRespository(db *database.DB) *TeamMemberRepository { + return &TeamMemberRepository{ + db: db, + } +} + +func (r *TeamMemberRepository) NewTx(tx pgx.Tx) *TeamMemberRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &TeamMemberRepository{ + db: txDB, + } +} + +func (r *TeamMemberRepository) GetTeamMembers(ctx context.Context, teamID uuid.UUID) ([]sqlc.GetTeamMembersRow, error) { + return r.db.Query.GetTeamMembers(ctx, teamID) +} + +func (r *TeamMemberRepository) GetTeamMemberByUser(ctx context.Context, userID uuid.UUID) (*sqlc.TeamMember, error) { + member, err := r.db.Query.GetTeamMemberByUserId(ctx, userID) + if err != nil && database.IsNotFound(err) { + return nil, ErrTeamMemberNotFound + } + return &member, err +} + +func (r *TeamMemberRepository) Create(ctx context.Context, params sqlc.CreateTeamMemberParams) (*sqlc.TeamMember, error) { + member, err := r.db.Query.CreateTeamMember(ctx, params) + + return &member, err +} + +func (r *TeamMemberRepository) Delete(ctx context.Context, params sqlc.RemoveTeamMemberParams) error { + return r.db.Query.RemoveTeamMember(ctx, params) +} diff --git a/apps/api/internal/database/repository/teams.go b/apps/api/internal/database/repository/teams.go new file mode 100644 index 00000000..6ca85b51 --- /dev/null +++ b/apps/api/internal/database/repository/teams.go @@ -0,0 +1,90 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrTeamNotFound = errors.New("team was not found") +) + +type TeamRepository struct { + db *database.DB +} + +func NewTeamRespository(db *database.DB) *TeamRepository { + return &TeamRepository{ + db: db, + } +} + +func (r *TeamRepository) NewTx(tx pgx.Tx) *TeamRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &TeamRepository{ + db: txDB, + } +} + +func (r *TeamRepository) Create(ctx context.Context, arg sqlc.CreateTeamParams) (*sqlc.Team, error) { + team, err := r.db.Query.CreateTeam(ctx, arg) + if err != nil { + return nil, err + } + + return &team, err +} + +func (r *TeamRepository) GetByID(ctx context.Context, teamID uuid.UUID) (*sqlc.Team, error) { + team, err := r.db.Query.GetTeamById(ctx, teamID) + if err != nil && database.IsNotFound(err) { + return nil, ErrTeamNotFound + } + + return &team, err +} + +func (r *TeamRepository) GetTeamByMember(ctx context.Context, userID uuid.UUID) (*sqlc.GetUserTeamRow, error) { + team, err := r.db.Query.GetUserTeam(ctx, userID) + + if err != nil && database.IsNotFound(err) { + return nil, ErrTeamNotFound + } + + return &team, err +} + +func (r *TeamRepository) GetTeamsWithMembers(ctx context.Context, params sqlc.ListTeamsWithMembersParams) ([]sqlc.ListTeamsWithMembersRow, error) { + return r.db.Query.ListTeamsWithMembers(ctx, params) +} + +func (r *TeamRepository) Delete(ctx context.Context, teamID uuid.UUID) error { + return r.db.Query.DeleteTeam(ctx, teamID) +} + +func (r *TeamRepository) Update(ctx context.Context, params sqlc.UpdateTeamByIdParams) (*sqlc.Team, error) { + // params := sqlc.UpdateTeamByIdParams{ + // ID: teamId, + // OwnerIDDoUpdate: ownerId != nil && *ownerId != uuid.Nil, + // NameDoUpdate: name != nil && *name != "", + // } + + // if ownerId != nil { + // params.OwnerID = ownerId + // } + // if name != nil { + // params.Name = *name + // } + + team, err := r.db.Query.UpdateTeamById(ctx, params) + return &team, err +} diff --git a/apps/api/internal/database/repository/users.go b/apps/api/internal/database/repository/users.go new file mode 100644 index 00000000..dc30e4e3 --- /dev/null +++ b/apps/api/internal/database/repository/users.go @@ -0,0 +1,118 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrUserNotFound = errors.New("user not found") +) + +type UserRepository struct { + db *database.DB +} + +func NewUserRepository(db *database.DB) *UserRepository { + return &UserRepository{ + db: db, + } +} + +// Call this to create a copy with transactional queries +func (r *UserRepository) NewTx(tx pgx.Tx) *UserRepository { + txDB := &database.DB{ + Pool: r.db.Pool, + Query: sqlc.New(tx), + } + + return &UserRepository{db: txDB} +} + +func (r *UserRepository) CreateUser(ctx context.Context, params sqlc.CreateUserParams) (*sqlc.User, error) { + user, err := r.db.Query.CreateUser(ctx, params) + if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *UserRepository) UpdateUser(ctx context.Context, params sqlc.UpdateUserParams) error { + err := r.db.Query.UpdateUser(ctx, params) + if err != nil { + if err == pgx.ErrNoRows { + return ErrUserNotFound + } + } + return err +} + +func (r *UserRepository) GetUserByID(ctx context.Context, id uuid.UUID) (*sqlc.User, error) { + user, err := r.db.Query.GetUserByID(ctx, id) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *UserRepository) GetUserByEmail(ctx context.Context, email string) (*sqlc.User, error) { + user, err := r.db.Query.GetUserByEmail(ctx, &email) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *UserRepository) GetUserEmailInfoById(ctx context.Context, id uuid.UUID) (*sqlc.GetUserEmailInfoByIdRow, error) { + row, err := r.db.Query.GetUserEmailInfoById(ctx, id) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { + return nil, err + } + + return &row, nil +} + +func (r *UserRepository) GetUserByRFID(ctx context.Context, rfid string) (*sqlc.User, error) { + user, err := r.db.Query.GetUserByRFID(ctx, &rfid) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrUserNotFound + } else if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *UserRepository) GetAllUsers(ctx context.Context, params sqlc.GetUsersParams) ([]sqlc.User, error) { + return r.db.Query.GetUsers(ctx, params) +} + +func (r *UserRepository) UpdateRole(ctx context.Context, updateRoleParams sqlc.UpdateRoleParams) error { + return r.db.Query.UpdateRole(ctx, updateRoleParams) +} + +func (r *UserRepository) RemoveRole(ctx context.Context, userID uuid.UUID) error { + return r.db.Query.RemoveRole(ctx, userID) +} + +func (r *UserRepository) UpdateCheckInTime(ctx context.Context, updateCheckInParams sqlc.UpdateCheckInTimeParams) error { + return r.db.Query.UpdateCheckInTime(ctx, updateCheckInParams) +} + +func (r *UserRepository) UpdateRFID(ctx context.Context, updateRFIDParams sqlc.UpdateRFIDParams) error { + return r.db.Query.UpdateRFID(ctx, updateRFIDParams) +} diff --git a/apps/api/internal/db/sqlc/accounts.sql.go b/apps/api/internal/database/sqlc/accounts.sql.go similarity index 91% rename from apps/api/internal/db/sqlc/accounts.sql.go rename to apps/api/internal/database/sqlc/accounts.sql.go index bba86324..9aaf5158 100644 --- a/apps/api/internal/db/sqlc/accounts.sql.go +++ b/apps/api/internal/database/sqlc/accounts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: accounts.sql package sqlc @@ -13,7 +13,7 @@ import ( ) const createAccount = `-- name: CreateAccount :one -INSERT INTO auth.accounts ( +INSERT INTO accounts ( user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope @@ -38,7 +38,7 @@ type CreateAccountParams struct { Scope *string `json:"scope"` } -func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) { +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { row := q.db.QueryRow(ctx, createAccount, arg.UserID, arg.ProviderID, @@ -51,7 +51,7 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A arg.RefreshTokenExpiresAt, arg.Scope, ) - var i AuthAccount + var i Account err := row.Scan( &i.ID, &i.UserID, @@ -71,7 +71,7 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A } const deleteAccount = `-- name: DeleteAccount :exec -DELETE FROM auth.accounts +DELETE FROM accounts WHERE provider_id = $1 AND account_id = $2 ` @@ -86,7 +86,7 @@ func (q *Queries) DeleteAccount(ctx context.Context, arg DeleteAccountParams) er } const getByProviderAndAccountID = `-- name: GetByProviderAndAccountID :one -SELECT id, user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope, created_at, updated_at FROM auth.accounts +SELECT id, user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope, created_at, updated_at FROM accounts WHERE provider_id = $1 AND account_id = $2 ` @@ -95,9 +95,9 @@ type GetByProviderAndAccountIDParams struct { AccountID string `json:"account_id"` } -func (q *Queries) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) { +func (q *Queries) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (Account, error) { row := q.db.QueryRow(ctx, getByProviderAndAccountID, arg.ProviderID, arg.AccountID) - var i AuthAccount + var i Account err := row.Scan( &i.ID, &i.UserID, @@ -117,19 +117,19 @@ func (q *Queries) GetByProviderAndAccountID(ctx context.Context, arg GetByProvid } const getByUserID = `-- name: GetByUserID :many -SELECT id, user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope, created_at, updated_at FROM auth.accounts +SELECT id, user_id, provider_id, account_id, hashed_password, access_token, refresh_token, id_token, access_token_expires_at, refresh_token_expires_at, scope, created_at, updated_at FROM accounts WHERE user_id = $1 ` -func (q *Queries) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) { +func (q *Queries) GetByUserID(ctx context.Context, userID uuid.UUID) ([]Account, error) { rows, err := q.db.Query(ctx, getByUserID, userID) if err != nil { return nil, err } defer rows.Close() - items := []AuthAccount{} + items := []Account{} for rows.Next() { - var i AuthAccount + var i Account if err := rows.Scan( &i.ID, &i.UserID, @@ -157,7 +157,7 @@ func (q *Queries) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAcco const getUserIDByDiscordAccountID = `-- name: GetUserIDByDiscordAccountID :one SELECT user_id -FROM auth.accounts +FROM accounts WHERE provider_id = 'discord' AND account_id = $1 ` @@ -169,7 +169,7 @@ func (q *Queries) GetUserIDByDiscordAccountID(ctx context.Context, accountID str } const updateTokens = `-- name: UpdateTokens :exec -UPDATE auth.accounts +UPDATE accounts SET access_token = $3, refresh_token = $4, id_token = $5, diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/database/sqlc/applications.sql.go similarity index 57% rename from apps/api/internal/db/sqlc/applications.sql.go rename to apps/api/internal/database/sqlc/applications.sql.go index d541ba04..9154fb7d 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/database/sqlc/applications.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: applications.sql package sqlc @@ -17,40 +17,32 @@ UPDATE applications SET assigned_reviewer_id = $1::uuid, status = 'under_review' WHERE user_id = ANY($2::uuid[]) - AND event_id = $3::uuid ` type AssignApplicationsToReviewerParams struct { ReviewerID uuid.UUID `json:"reviewer_id"` ApplicationIds []uuid.UUID `json:"application_ids"` - EventID uuid.UUID `json:"event_id"` } func (q *Queries) AssignApplicationsToReviewer(ctx context.Context, arg AssignApplicationsToReviewerParams) error { - _, err := q.db.Exec(ctx, assignApplicationsToReviewer, arg.ReviewerID, arg.ApplicationIds, arg.EventID) + _, err := q.db.Exec(ctx, assignApplicationsToReviewer, arg.ReviewerID, arg.ApplicationIds) return err } const createApplication = `-- name: CreateApplication :one -INSERT INTO applications ( - user_id, event_id -) VALUES ( - $1, $2 -) -RETURNING user_id, event_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time +INSERT INTO applications (user_id, hackathon_id) VALUES ($1, $2) RETURNING user_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time, hackathon_id ` type CreateApplicationParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` + UserID uuid.UUID `json:"user_id"` + HackathonID string `json:"hackathon_id"` } func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) { - row := q.db.QueryRow(ctx, createApplication, arg.UserID, arg.EventID) + row := q.db.QueryRow(ctx, createApplication, arg.UserID, arg.HackathonID) var i Application err := row.Scan( &i.UserID, - &i.EventID, &i.Status, &i.Application, &i.CreatedAt, @@ -61,41 +53,29 @@ func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationPa &i.PassionRating, &i.AssignedReviewerID, &i.WaitlistJoinTime, + &i.HackathonID, ) return i, err } const deleteApplication = `-- name: DeleteApplication :exec -DELETE FROM applications -WHERE user_id = $1 AND event_id = $2 +DELETE FROM applications WHERE user_id = $1 ` -type DeleteApplicationParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error { - _, err := q.db.Exec(ctx, deleteApplication, arg.UserID, arg.EventID) +func (q *Queries) DeleteApplication(ctx context.Context, userID uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteApplication, userID) return err } -const getApplicationByUserAndEventID = `-- name: GetApplicationByUserAndEventID :one -SELECT user_id, event_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time FROM applications -WHERE user_id = $1 AND event_id = $2 +const getApplicationByUserId = `-- name: GetApplicationByUserId :one +SELECT user_id, status, application, created_at, saved_at, updated_at, submitted_at, experience_rating, passion_rating, assigned_reviewer_id, waitlist_join_time, hackathon_id FROM applications WHERE user_id = $1 ` -type GetApplicationByUserAndEventIDParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) { - row := q.db.QueryRow(ctx, getApplicationByUserAndEventID, arg.UserID, arg.EventID) +func (q *Queries) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (Application, error) { + row := q.db.QueryRow(ctx, getApplicationByUserId, userID) var i Application err := row.Scan( &i.UserID, - &i.EventID, &i.Status, &i.Application, &i.CreatedAt, @@ -106,6 +86,7 @@ func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApp &i.PassionRating, &i.AssignedReviewerID, &i.WaitlistJoinTime, + &i.HackathonID, ) return i, err } @@ -114,20 +95,15 @@ const joinWaitlist = `-- name: JoinWaitlist :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' -WHERE user_id = $1 AND event_id = $2 +WHERE user_id = $1 ` -type JoinWaitlistParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) JoinWaitlist(ctx context.Context, arg JoinWaitlistParams) error { - _, err := q.db.Exec(ctx, joinWaitlist, arg.UserID, arg.EventID) +func (q *Queries) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { + _, err := q.db.Exec(ctx, joinWaitlist, userID) return err } -const listAdmissionCandidatesByEvent = `-- name: ListAdmissionCandidatesByEvent :many +const listAdmissionCandidates = `-- name: ListAdmissionCandidates :many SELECT a.user_id, a.passion_rating, a.experience_rating, @@ -138,14 +114,12 @@ LEFT JOIN team_members tm ON tm.user_id = a.user_id LEFT JOIN teams t ON t.id = tm.team_id - AND t.event_id = a.event_id -WHERE a.event_id = $1 - AND a.status = 'under_review' +WHERE a.status = 'under_review' AND a.passion_rating IS NOT NULL AND a.experience_rating IS NOT NULL ` -type ListAdmissionCandidatesByEventRow struct { +type ListAdmissionCandidatesRow struct { UserID uuid.UUID `json:"user_id"` PassionRating *int32 `json:"passion_rating"` ExperienceRating *int32 `json:"experience_rating"` @@ -153,15 +127,15 @@ type ListAdmissionCandidatesByEventRow struct { TeamID *uuid.UUID `json:"team_id"` } -func (q *Queries) ListAdmissionCandidatesByEvent(ctx context.Context, eventID uuid.UUID) ([]ListAdmissionCandidatesByEventRow, error) { - rows, err := q.db.Query(ctx, listAdmissionCandidatesByEvent, eventID) +func (q *Queries) ListAdmissionCandidates(ctx context.Context) ([]ListAdmissionCandidatesRow, error) { + rows, err := q.db.Query(ctx, listAdmissionCandidates) if err != nil { return nil, err } defer rows.Close() - items := []ListAdmissionCandidatesByEventRow{} + items := []ListAdmissionCandidatesRow{} for rows.Next() { - var i ListAdmissionCandidatesByEventRow + var i ListAdmissionCandidatesRow if err := rows.Scan( &i.UserID, &i.PassionRating, @@ -179,34 +153,28 @@ func (q *Queries) ListAdmissionCandidatesByEvent(ctx context.Context, eventID uu return items, nil } -const listApplicationByReviewerAndEvent = `-- name: ListApplicationByReviewerAndEvent :many +const listApplicationByReviewer = `-- name: ListApplicationByReviewer :many SELECT user_id, passion_rating, experience_rating FROM applications WHERE assigned_reviewer_id = $1 - AND event_id = $2 AND status IN ('under_review') ORDER BY user_id ASC ` -type ListApplicationByReviewerAndEventParams struct { - AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` - EventID uuid.UUID `json:"event_id"` -} - -type ListApplicationByReviewerAndEventRow struct { +type ListApplicationByReviewerRow struct { UserID uuid.UUID `json:"user_id"` PassionRating *int32 `json:"passion_rating"` ExperienceRating *int32 `json:"experience_rating"` } -func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg ListApplicationByReviewerAndEventParams) ([]ListApplicationByReviewerAndEventRow, error) { - rows, err := q.db.Query(ctx, listApplicationByReviewerAndEvent, arg.AssignedReviewerID, arg.EventID) +func (q *Queries) ListApplicationByReviewer(ctx context.Context, assignedReviewerID *uuid.UUID) ([]ListApplicationByReviewerRow, error) { + rows, err := q.db.Query(ctx, listApplicationByReviewer, assignedReviewerID) if err != nil { return nil, err } defer rows.Close() - items := []ListApplicationByReviewerAndEventRow{} + items := []ListApplicationByReviewerRow{} for rows.Next() { - var i ListApplicationByReviewerAndEventRow + var i ListApplicationByReviewerRow if err := rows.Scan(&i.UserID, &i.PassionRating, &i.ExperienceRating); err != nil { return nil, err } @@ -218,21 +186,20 @@ func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg Lis return items, nil } -const listAvailableApplicationsForEvent = `-- name: ListAvailableApplicationsForEvent :many +const listAvailableApplications = `-- name: ListAvailableApplications :many SELECT user_id FROM applications -WHERE event_id = $1 - AND status = 'submitted' +WHERE status = 'submitted' AND experience_rating IS NULL AND passion_rating IS NULL ORDER BY user_id ASC ` -// An application is considered "available" for an event if the application has a status of submitted and has not been reviewed yet. +// An application is considered "available" if the application has a status of submitted and has not been reviewed yet. // For optimization purposes, we only select the application IDs. -func (q *Queries) ListAvailableApplicationsForEvent(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, listAvailableApplicationsForEvent, eventID) +func (q *Queries) ListAvailableApplications(ctx context.Context) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, listAvailableApplications) if err != nil { return nil, err } @@ -251,16 +218,15 @@ func (q *Queries) ListAvailableApplicationsForEvent(ctx context.Context, eventID return items, nil } -const listNonReviewedApplicationsByEvent = `-- name: ListNonReviewedApplicationsByEvent :many +const listNonReviewedApplications = `-- name: ListNonReviewedApplications :many SELECT user_id FROM applications -WHERE event_id = $1 - AND status = 'under_review' +WHERE status = 'under_review' AND (passion_rating IS NULL OR experience_rating IS NULL) ` -func (q *Queries) ListNonReviewedApplicationsByEvent(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, listNonReviewedApplicationsByEvent, eventID) +func (q *Queries) ListNonReviewedApplications(ctx context.Context) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, listNonReviewedApplications) if err != nil { return nil, err } @@ -286,52 +252,44 @@ SET assigned_reviewer_id = NULL, experience_rating = NULL, passion_rating = NULL WHERE status NOT IN ('submitted', 'started') - AND event_id = $1 ` -func (q *Queries) ResetApplicationReviews(ctx context.Context, eventID uuid.UUID) error { - _, err := q.db.Exec(ctx, resetApplicationReviews, eventID) +func (q *Queries) ResetApplicationReviews(ctx context.Context) error { + _, err := q.db.Exec(ctx, resetApplicationReviews) return err } -const transitionAcceptedApplicationsToWaitlistByEventID = `-- name: TransitionAcceptedApplicationsToWaitlistByEventID :exec +const transitionAcceptedApplicationsToWaitlist = `-- name: TransitionAcceptedApplicationsToWaitlist :exec UPDATE applications SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), status = 'waitlisted' -WHERE event_id = $1::uuid - AND status = 'accepted' +WHERE status = 'accepted' AND user_id IN ( - SELECT user_id from event_roles AS er - WHERE er.role = 'applicant' + SELECT id from users + WHERE role = 'applicant' ) ` -func (q *Queries) TransitionAcceptedApplicationsToWaitlistByEventID(ctx context.Context, eventID uuid.UUID) error { - _, err := q.db.Exec(ctx, transitionAcceptedApplicationsToWaitlistByEventID, eventID) +func (q *Queries) TransitionAcceptedApplicationsToWaitlist(ctx context.Context) error { + _, err := q.db.Exec(ctx, transitionAcceptedApplicationsToWaitlist) return err } -const transitionWaitlistedApplicationsToAcceptedByEventID = `-- name: TransitionWaitlistedApplicationsToAcceptedByEventID :many +const transitionWaitlistedApplicationsToAccepted = `-- name: TransitionWaitlistedApplicationsToAccepted :many UPDATE applications SET waitlist_join_time = NULL, status = 'accepted' WHERE user_id IN ( SELECT user_id FROM applications - WHERE event_id = $1::uuid - AND status = 'waitlisted' + WHERE status = 'waitlisted' ORDER BY waitlist_join_time ASC - LIMIT $2::int + LIMIT $1::int ) RETURNING user_id ` -type TransitionWaitlistedApplicationsToAcceptedByEventIDParams struct { - EventID uuid.UUID `json:"event_id"` - Acceptancecount int32 `json:"acceptancecount"` -} - -func (q *Queries) TransitionWaitlistedApplicationsToAcceptedByEventID(ctx context.Context, arg TransitionWaitlistedApplicationsToAcceptedByEventIDParams) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, transitionWaitlistedApplicationsToAcceptedByEventID, arg.EventID, arg.Acceptancecount) +func (q *Queries) TransitionWaitlistedApplicationsToAccepted(ctx context.Context, acceptancecount int32) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, transitionWaitlistedApplicationsToAccepted, acceptancecount) if err != nil { return nil, err } @@ -361,7 +319,7 @@ SET experience_rating = CASE WHEN $11::boolean THEN $12::INT ELSE experience_rating END, passion_rating = CASE WHEN $13::boolean THEN $14::INT ELSE passion_rating END WHERE - user_id = $15 AND event_id = $16 + user_id = $15 ` type UpdateApplicationParams struct { @@ -380,7 +338,6 @@ type UpdateApplicationParams struct { PassionRatingDoUpdate bool `json:"passion_rating_do_update"` PassionRating int32 `json:"passion_rating"` UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` } func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error { @@ -400,25 +357,22 @@ func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationPa arg.PassionRatingDoUpdate, arg.PassionRating, arg.UserID, - arg.EventID, ) return err } -const updateApplicationStatusByEventID = `-- name: UpdateApplicationStatusByEventID :exec +const updateApplicationStatus = `-- name: UpdateApplicationStatus :exec UPDATE applications SET status = $1::application_status -WHERE event_id = $2::uuid - AND user_id = ANY($3::uuid[]) +WHERE user_id = ANY($2::uuid[]) ` -type UpdateApplicationStatusByEventIDParams struct { +type UpdateApplicationStatusParams struct { Status ApplicationStatus `json:"status"` - EventID uuid.UUID `json:"event_id"` UserIds []uuid.UUID `json:"user_ids"` } -func (q *Queries) UpdateApplicationStatusByEventID(ctx context.Context, arg UpdateApplicationStatusByEventIDParams) error { - _, err := q.db.Exec(ctx, updateApplicationStatusByEventID, arg.Status, arg.EventID, arg.UserIds) +func (q *Queries) UpdateApplicationStatus(ctx context.Context, arg UpdateApplicationStatusParams) error { + _, err := q.db.Exec(ctx, updateApplicationStatus, arg.Status, arg.UserIds) return err } diff --git a/apps/api/internal/database/sqlc/bat_runs.sql.go b/apps/api/internal/database/sqlc/bat_runs.sql.go new file mode 100644 index 00000000..3829be09 --- /dev/null +++ b/apps/api/internal/database/sqlc/bat_runs.sql.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: bat_runs.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const addBatRun = `-- name: AddBatRun :one +INSERT INTO bat_runs (hackathon_id) VALUES ($1) RETURNING id, accepted_applicants, rejected_applicants, status, created_at, completed_at, hackathon_id +` + +func (q *Queries) AddBatRun(ctx context.Context, hackathonID string) (BatRun, error) { + row := q.db.QueryRow(ctx, addBatRun, hackathonID) + var i BatRun + err := row.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + &i.HackathonID, + ) + return i, err +} + +const deleteBatRunById = `-- name: DeleteBatRunById :execrows +DELETE FROM bat_runs +WHERE id = $1 +` + +func (q *Queries) DeleteBatRunById(ctx context.Context, id uuid.UUID) (int64, error) { + result, err := q.db.Exec(ctx, deleteBatRunById, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getBatRunById = `-- name: GetBatRunById :one +SELECT id, accepted_applicants, rejected_applicants, status, created_at, completed_at, hackathon_id +FROM bat_runs +WHERE id = $1 +` + +func (q *Queries) GetBatRunById(ctx context.Context, id uuid.UUID) (BatRun, error) { + row := q.db.QueryRow(ctx, getBatRunById, id) + var i BatRun + err := row.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + &i.HackathonID, + ) + return i, err +} + +const getBatRuns = `-- name: GetBatRuns :many +SELECT + id, accepted_applicants, rejected_applicants, status, created_at, completed_at, hackathon_id +FROM bat_runs +ORDER BY created_at DESC +` + +func (q *Queries) GetBatRuns(ctx context.Context) ([]BatRun, error) { + rows, err := q.db.Query(ctx, getBatRuns) + if err != nil { + return nil, err + } + defer rows.Close() + items := []BatRun{} + for rows.Next() { + var i BatRun + if err := rows.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + &i.HackathonID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateBatRunById = `-- name: UpdateBatRunById :exec +UPDATE bat_runs +SET + accepted_applicants = CASE WHEN $1::boolean THEN $2 ELSE accepted_applicants END, + rejected_applicants = CASE WHEN $3::boolean THEN $4 ELSE rejected_applicants END, + status = CASE WHEN $5::boolean THEN $6 ELSE status END, + created_at = CASE WHEN $7::boolean THEN $8 ELSE created_at END +WHERE + id = $9::uuid +RETURNING id, accepted_applicants, rejected_applicants, status, created_at, completed_at, hackathon_id +` + +type UpdateBatRunByIdParams struct { + AcceptedApplicantsDoUpdate bool `json:"accepted_applicants_do_update"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicantsDoUpdate bool `json:"rejected_applicants_do_update"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + StatusDoUpdate bool `json:"status_do_update"` + Status BatRunStatus `json:"status"` + CreatedAtDoUpdate bool `json:"created_at_do_update"` + CreatedAt time.Time `json:"created_at"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateBatRunById(ctx context.Context, arg UpdateBatRunByIdParams) error { + _, err := q.db.Exec(ctx, updateBatRunById, + arg.AcceptedApplicantsDoUpdate, + arg.AcceptedApplicants, + arg.RejectedApplicantsDoUpdate, + arg.RejectedApplicants, + arg.StatusDoUpdate, + arg.Status, + arg.CreatedAtDoUpdate, + arg.CreatedAt, + arg.ID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/db.go b/apps/api/internal/database/sqlc/db.go similarity index 96% rename from apps/api/internal/db/sqlc/db.go rename to apps/api/internal/database/sqlc/db.go index 27251088..7a565074 100644 --- a/apps/api/internal/db/sqlc/db.go +++ b/apps/api/internal/database/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc diff --git a/apps/api/internal/database/sqlc/hackathons.sql.go b/apps/api/internal/database/sqlc/hackathons.sql.go new file mode 100644 index 00000000..a5e82e24 --- /dev/null +++ b/apps/api/internal/database/sqlc/hackathons.sql.go @@ -0,0 +1,322 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: hackathons.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createHackathon = `-- name: CreateHackathon :one +INSERT INTO hackathons ( + id, name, + application_open, application_close, + start_time, end_time, + description, location, location_url, max_attendees, + rsvp_deadline, decision_release, is_active +) VALUES ( + -- FIXME: The second parameter in coalesce MUST be the default value created in the schema. I have not found a more automated way to insert the default value. + $1, $2, + $3, $4, + $5, $6, + coalesce($7, NULL), + coalesce($8, NULL), + coalesce($9, NULL), + coalesce($10, NULL::INT), + coalesce($11, NULL::TIMESTAMPTZ), + coalesce($12, NULL::TIMESTAMPTZ), + coalesce($13, false) +) +RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, is_active, created_at, updated_at, banner, application_review_started +` + +type CreateHackathonParams struct { + ID string `json:"id"` + Name string `json:"name"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Description interface{} `json:"description"` + Location interface{} `json:"location"` + LocationUrl interface{} `json:"location_url"` + MaxAttendees interface{} `json:"max_attendees"` + RsvpDeadline interface{} `json:"rsvp_deadline"` + DecisionRelease interface{} `json:"decision_release"` + IsActive interface{} `json:"is_active"` +} + +func (q *Queries) CreateHackathon(ctx context.Context, arg CreateHackathonParams) (Hackathon, error) { + row := q.db.QueryRow(ctx, createHackathon, + arg.ID, + arg.Name, + arg.ApplicationOpen, + arg.ApplicationClose, + arg.StartTime, + arg.EndTime, + arg.Description, + arg.Location, + arg.LocationUrl, + arg.MaxAttendees, + arg.RsvpDeadline, + arg.DecisionRelease, + arg.IsActive, + ) + var i Hackathon + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + &i.Banner, + &i.ApplicationReviewStarted, + ) + return i, err +} + +const getAttendeeCount = `-- name: GetAttendeeCount :one +SELECT COUNT(*) FROM users +WHERE role = 'attendee' +` + +func (q *Queries) GetAttendeeCount(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, getAttendeeCount) + var count int64 + err := row.Scan(&count) + return count, err +} + +const getAttendeeUserIds = `-- name: GetAttendeeUserIds :many +SELECT id FROM users +WHERE role = 'attendee' +` + +func (q *Queries) GetAttendeeUserIds(ctx context.Context) ([]uuid.UUID, error) { + rows, err := q.db.Query(ctx, getAttendeeUserIds) + if err != nil { + return nil, err + } + defer rows.Close() + items := []uuid.UUID{} + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAttendeesWithDiscord = `-- name: GetAttendeesWithDiscord :many +SELECT + a.account_id as discord_id, + u.id as user_id, + u.name, + u.email +FROM users u +JOIN accounts a ON u.id = a.user_id +WHERE u.role = 'attendee' + AND a.provider_id = 'discord' +` + +type GetAttendeesWithDiscordRow struct { + DiscordID string `json:"discord_id"` + UserID uuid.UUID `json:"user_id"` + Name string `json:"name"` + Email *string `json:"email"` +} + +func (q *Queries) GetAttendeesWithDiscord(ctx context.Context) ([]GetAttendeesWithDiscordRow, error) { + rows, err := q.db.Query(ctx, getAttendeesWithDiscord) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetAttendeesWithDiscordRow{} + for rows.Next() { + var i GetAttendeesWithDiscordRow + if err := rows.Scan( + &i.DiscordID, + &i.UserID, + &i.Name, + &i.Email, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getHackathon = `-- name: GetHackathon :one +SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, is_active, created_at, updated_at, banner, application_review_started FROM hackathons WHERE is_active = true +` + +func (q *Queries) GetHackathon(ctx context.Context) (Hackathon, error) { + row := q.db.QueryRow(ctx, getHackathon) + var i Hackathon + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + &i.Banner, + &i.ApplicationReviewStarted, + ) + return i, err +} + +const getStaff = `-- name: GetStaff :many +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +WHERE role IN ('admin', 'staff') +` + +func (q *Queries) GetStaff(ctx context.Context) ([]User, error) { + rows, err := q.db.Query(ctx, getStaff) + if err != nil { + return nil, err + } + defer rows.Close() + items := []User{} + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.EmailVerified, + &i.Onboarded, + &i.Image, + &i.CreatedAt, + &i.UpdatedAt, + &i.PreferredEmail, + &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, + &i.Role, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateHackathon = `-- name: UpdateHackathon :exec +UPDATE hackathons +SET + name = CASE WHEN $1::boolean THEN $2 ELSE name END, + description = CASE WHEN $3::boolean THEN $4 ELSE description END, + location = CASE WHEN $5::boolean THEN $6 ELSE location END, + location_url = CASE WHEN $7::boolean THEN $8 ELSE location_url END, + max_attendees = CASE WHEN $9::boolean THEN $10 ELSE max_attendees END, + application_open = CASE WHEN $11::boolean THEN $12 ELSE application_open END, + application_close = CASE WHEN $13::boolean THEN $14 ELSE application_close END, + rsvp_deadline = CASE WHEN $15::boolean THEN $16 ELSE rsvp_deadline END, + decision_release = CASE WHEN $17::boolean THEN $18 ELSE decision_release END, + start_time = CASE WHEN $19::boolean THEN $20 ELSE start_time END, + end_time = CASE WHEN $21::boolean THEN $22 ELSE end_time END, + banner = CASE WHEN $23::boolean THEN $24 ELSE banner END, + application_review_started = CASE WHEN $25::boolean THEN $26 ELSE application_review_started END +WHERE is_active = true +RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, is_active, created_at, updated_at, banner, application_review_started +` + +type UpdateHackathonParams struct { + NameDoUpdate bool `json:"name_do_update"` + Name string `json:"name"` + DescriptionDoUpdate bool `json:"description_do_update"` + Description *string `json:"description"` + LocationDoUpdate bool `json:"location_do_update"` + Location *string `json:"location"` + LocationUrlDoUpdate bool `json:"location_url_do_update"` + LocationUrl *string `json:"location_url"` + MaxAttendeesDoUpdate bool `json:"max_attendees_do_update"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpenDoUpdate bool `json:"application_open_do_update"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationCloseDoUpdate bool `json:"application_close_do_update"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadlineDoUpdate bool `json:"rsvp_deadline_do_update"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionReleaseDoUpdate bool `json:"decision_release_do_update"` + DecisionRelease *time.Time `json:"decision_release"` + StartTimeDoUpdate bool `json:"start_time_do_update"` + StartTime time.Time `json:"start_time"` + EndTimeDoUpdate bool `json:"end_time_do_update"` + EndTime time.Time `json:"end_time"` + BannerDoUpdate bool `json:"banner_do_update"` + Banner *string `json:"banner"` + ApplicationReviewStartedDoUpdate bool `json:"application_review_started_do_update"` + ApplicationReviewStarted bool `json:"application_review_started"` +} + +func (q *Queries) UpdateHackathon(ctx context.Context, arg UpdateHackathonParams) error { + _, err := q.db.Exec(ctx, updateHackathon, + arg.NameDoUpdate, + arg.Name, + arg.DescriptionDoUpdate, + arg.Description, + arg.LocationDoUpdate, + arg.Location, + arg.LocationUrlDoUpdate, + arg.LocationUrl, + arg.MaxAttendeesDoUpdate, + arg.MaxAttendees, + arg.ApplicationOpenDoUpdate, + arg.ApplicationOpen, + arg.ApplicationCloseDoUpdate, + arg.ApplicationClose, + arg.RsvpDeadlineDoUpdate, + arg.RsvpDeadline, + arg.DecisionReleaseDoUpdate, + arg.DecisionRelease, + arg.StartTimeDoUpdate, + arg.StartTime, + arg.EndTimeDoUpdate, + arg.EndTime, + arg.BannerDoUpdate, + arg.Banner, + arg.ApplicationReviewStartedDoUpdate, + arg.ApplicationReviewStarted, + ) + return err +} diff --git a/apps/api/internal/database/sqlc/interest_submissions.sql.go b/apps/api/internal/database/sqlc/interest_submissions.sql.go new file mode 100644 index 00000000..1f40992b --- /dev/null +++ b/apps/api/internal/database/sqlc/interest_submissions.sql.go @@ -0,0 +1,43 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: interest_submissions.sql + +package sqlc + +import ( + "context" +) + +const addEmail = `-- name: AddEmail :one +INSERT INTO interest_submissions ( + email, + source, + hackathon_id +) VALUES ( + $1, $2, $3 +) +RETURNING id, email, created_at, source, hackathon_id +` + +type AddEmailParams struct { + Email string `json:"email"` + Source *string `json:"source"` + HackathonID string `json:"hackathon_id"` +} + +// Adds a new email to the mailing list for a specific user. +// The unique constraint on `email` will prevent duplicates. +// Returns the newly created email record. +func (q *Queries) AddEmail(ctx context.Context, arg AddEmailParams) (InterestSubmission, error) { + row := q.db.QueryRow(ctx, addEmail, arg.Email, arg.Source, arg.HackathonID) + var i InterestSubmission + err := row.Scan( + &i.ID, + &i.Email, + &i.CreatedAt, + &i.Source, + &i.HackathonID, + ) + return i, err +} diff --git a/apps/api/internal/database/sqlc/models.go b/apps/api/internal/database/sqlc/models.go new file mode 100644 index 00000000..0d467cab --- /dev/null +++ b/apps/api/internal/database/sqlc/models.go @@ -0,0 +1,390 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package sqlc + +import ( + "database/sql/driver" + "fmt" + "time" + + "github.com/google/uuid" +) + +type ApplicationStatus string + +const ( + ApplicationStatusStarted ApplicationStatus = "started" + ApplicationStatusSubmitted ApplicationStatus = "submitted" + ApplicationStatusUnderReview ApplicationStatus = "under_review" + ApplicationStatusAccepted ApplicationStatus = "accepted" + ApplicationStatusRejected ApplicationStatus = "rejected" + ApplicationStatusWaitlisted ApplicationStatus = "waitlisted" + ApplicationStatusWithdrawn ApplicationStatus = "withdrawn" +) + +func (e *ApplicationStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ApplicationStatus(s) + case string: + *e = ApplicationStatus(s) + default: + return fmt.Errorf("unsupported scan type for ApplicationStatus: %T", src) + } + return nil +} + +type NullApplicationStatus struct { + ApplicationStatus ApplicationStatus `json:"application_status"` + Valid bool `json:"valid"` // Valid is true if ApplicationStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullApplicationStatus) Scan(value interface{}) error { + if value == nil { + ns.ApplicationStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ApplicationStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullApplicationStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ApplicationStatus), nil +} + +type BatRunStatus string + +const ( + BatRunStatusRunning BatRunStatus = "running" + BatRunStatusCompleted BatRunStatus = "completed" + BatRunStatusFailed BatRunStatus = "failed" +) + +func (e *BatRunStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = BatRunStatus(s) + case string: + *e = BatRunStatus(s) + default: + return fmt.Errorf("unsupported scan type for BatRunStatus: %T", src) + } + return nil +} + +type NullBatRunStatus struct { + BatRunStatus BatRunStatus `json:"bat_run_status"` + Valid bool `json:"valid"` // Valid is true if BatRunStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullBatRunStatus) Scan(value interface{}) error { + if value == nil { + ns.BatRunStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.BatRunStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullBatRunStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.BatRunStatus), nil +} + +type TeamInvitationStatus string + +const ( + TeamInvitationStatusPending TeamInvitationStatus = "pending" + TeamInvitationStatusAccepted TeamInvitationStatus = "accepted" + TeamInvitationStatusExpired TeamInvitationStatus = "expired" + TeamInvitationStatusRejected TeamInvitationStatus = "rejected" +) + +func (e *TeamInvitationStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = TeamInvitationStatus(s) + case string: + *e = TeamInvitationStatus(s) + default: + return fmt.Errorf("unsupported scan type for TeamInvitationStatus: %T", src) + } + return nil +} + +type NullTeamInvitationStatus struct { + TeamInvitationStatus TeamInvitationStatus `json:"team_invitation_status"` + Valid bool `json:"valid"` // Valid is true if TeamInvitationStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullTeamInvitationStatus) Scan(value interface{}) error { + if value == nil { + ns.TeamInvitationStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.TeamInvitationStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullTeamInvitationStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.TeamInvitationStatus), nil +} + +type TeamJoinRequestStatus string + +const ( + TeamJoinRequestStatusPending TeamJoinRequestStatus = "pending" + TeamJoinRequestStatusApproved TeamJoinRequestStatus = "approved" + TeamJoinRequestStatusRejected TeamJoinRequestStatus = "rejected" +) + +func (e *TeamJoinRequestStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = TeamJoinRequestStatus(s) + case string: + *e = TeamJoinRequestStatus(s) + default: + return fmt.Errorf("unsupported scan type for TeamJoinRequestStatus: %T", src) + } + return nil +} + +type NullTeamJoinRequestStatus struct { + TeamJoinRequestStatus TeamJoinRequestStatus `json:"team_join_request_status"` + Valid bool `json:"valid"` // Valid is true if TeamJoinRequestStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullTeamJoinRequestStatus) Scan(value interface{}) error { + if value == nil { + ns.TeamJoinRequestStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.TeamJoinRequestStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullTeamJoinRequestStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.TeamJoinRequestStatus), nil +} + +type UserRole string + +const ( + UserRoleAdmin UserRole = "admin" + UserRoleStaff UserRole = "staff" + UserRoleAttendee UserRole = "attendee" + UserRoleApplicant UserRole = "applicant" + UserRoleVisitor UserRole = "visitor" +) + +func (e *UserRole) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UserRole(s) + case string: + *e = UserRole(s) + default: + return fmt.Errorf("unsupported scan type for UserRole: %T", src) + } + return nil +} + +type NullUserRole struct { + UserRole UserRole `json:"user_role"` + Valid bool `json:"valid"` // Valid is true if UserRole is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullUserRole) Scan(value interface{}) error { + if value == nil { + ns.UserRole, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.UserRole.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullUserRole) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.UserRole), nil +} + +type Account struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ProviderID string `json:"provider_id"` + AccountID string `json:"account_id"` + HashedPassword *string `json:"hashed_password"` + AccessToken *string `json:"access_token"` + RefreshToken *string `json:"refresh_token"` + IDToken *string `json:"id_token"` + AccessTokenExpiresAt *time.Time `json:"access_token_expires_at"` + RefreshTokenExpiresAt *time.Time `json:"refresh_token_expires_at"` + Scope *string `json:"scope"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Application struct { + UserID uuid.UUID `json:"user_id"` + Status ApplicationStatus `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"created_at"` + SavedAt time.Time `json:"saved_at"` + UpdatedAt time.Time `json:"updated_at"` + SubmittedAt *time.Time `json:"submitted_at"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` + AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` + WaitlistJoinTime *time.Time `json:"waitlist_join_time"` + HackathonID string `json:"hackathon_id"` +} + +type BatRun struct { + ID uuid.UUID `json:"id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + Status BatRunStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at"` + HackathonID string `json:"hackathon_id"` +} + +type Hackathon struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Banner *string `json:"banner"` + ApplicationReviewStarted bool `json:"application_review_started"` +} + +type InterestSubmission struct { + ID uuid.UUID `json:"id"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Source *string `json:"source"` + HackathonID string `json:"hackathon_id"` +} + +type Redeemable struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Amount int32 `json:"amount"` + MaxUserAmount int32 `json:"max_user_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HackathonID string `json:"hackathon_id"` +} + +type Session struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ExpiresAt time.Time `json:"expires_at"` + IpAddress *string `json:"ip_address"` + UserAgent *string `json:"user_agent"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastUsedAt time.Time `json:"last_used_at"` +} + +type Team struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + OwnerID *uuid.UUID `json:"owner_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HackathonID string `json:"hackathon_id"` +} + +type TeamInvitation struct { + ID uuid.UUID `json:"id"` + TeamID uuid.UUID `json:"team_id"` + InvitedByUserID uuid.UUID `json:"invited_by_user_id"` + InvitedEmail string `json:"invited_email"` + InvitedUserID *uuid.UUID `json:"invited_user_id"` + Status TeamInvitationStatus `json:"status"` + ExpiresAt *time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TeamJoinRequest struct { + ID uuid.UUID `json:"id"` + TeamID uuid.UUID `json:"team_id"` + UserID uuid.UUID `json:"user_id"` + RequestMessage *string `json:"request_message"` + Status TeamJoinRequestStatus `json:"status"` + ProcessedByUserID *uuid.UUID `json:"processed_by_user_id"` + ProcessedAt *time.Time `json:"processed_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TeamMember struct { + UserID uuid.UUID `json:"user_id"` + TeamID uuid.UUID `json:"team_id"` + JoinedAt time.Time `json:"joined_at"` +} + +type User struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Email *string `json:"email"` + EmailVerified bool `json:"email_verified"` + Onboarded bool `json:"onboarded"` + Image *string `json:"image"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PreferredEmail *string `json:"preferred_email"` + EmailConsent bool `json:"email_consent"` + CheckedInAt *time.Time `json:"checked_in_at"` + Rfid *string `json:"rfid"` + RoleAssignedAt *time.Time `json:"role_assigned_at"` + Role UserRole `json:"role"` +} + +type UserRedemption struct { + UserID uuid.UUID `json:"user_id"` + RedeemableID uuid.UUID `json:"redeemable_id"` + Amount int32 `json:"amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HackathonID string `json:"hackathon_id"` +} diff --git a/apps/api/internal/db/sqlc/redeemables.sql.go b/apps/api/internal/database/sqlc/redeemables.sql.go similarity index 69% rename from apps/api/internal/db/sqlc/redeemables.sql.go rename to apps/api/internal/database/sqlc/redeemables.sql.go index 3086b7f0..26e52067 100644 --- a/apps/api/internal/db/sqlc/redeemables.sql.go +++ b/apps/api/internal/database/sqlc/redeemables.sql.go @@ -1,47 +1,46 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: redeemables.sql package sqlc import ( "context" + "time" "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" ) const createRedeemable = `-- name: CreateRedeemable :one -INSERT INTO redeemables (event_id, name, amount, max_user_amount) +INSERT INTO redeemables (name, amount, max_user_amount, hackathon_id) VALUES ($1, $2, $3, $4) -RETURNING id, event_id, name, amount, max_user_amount, created_at, updated_at +RETURNING id, name, amount, max_user_amount, created_at, updated_at, hackathon_id ` type CreateRedeemableParams struct { - EventID uuid.UUID `json:"event_id"` - Name string `json:"name"` - Amount int32 `json:"amount"` - MaxUserAmount int32 `json:"max_user_amount"` + Name string `json:"name"` + Amount int32 `json:"amount"` + MaxUserAmount int32 `json:"max_user_amount"` + HackthonID string `json:"hackthon_id"` } -// Create a new redeemable for an event func (q *Queries) CreateRedeemable(ctx context.Context, arg CreateRedeemableParams) (Redeemable, error) { row := q.db.QueryRow(ctx, createRedeemable, - arg.EventID, arg.Name, arg.Amount, arg.MaxUserAmount, + arg.HackthonID, ) var i Redeemable err := row.Scan( &i.ID, - &i.EventID, &i.Name, &i.Amount, &i.MaxUserAmount, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } @@ -51,15 +50,13 @@ DELETE FROM redeemables WHERE id = $1 ` -// Delete a redeemable by id func (q *Queries) DeleteRedeemable(ctx context.Context, id uuid.UUID) error { _, err := q.db.Exec(ctx, deleteRedeemable, id) return err } -const getRedeemablesByEventID = `-- name: GetRedeemablesByEventID :many -SELECT r.id, -r.event_id, +const getRedeemables = `-- name: GetRedeemables :many +SELECT r.id, r.name, r.amount AS total_stock, r.max_user_amount, @@ -68,34 +65,30 @@ r.updated_at, COALESCE(SUM(ur.amount), 0) AS total_redeemed FROM redeemables r LEFT JOIN user_redemptions ur ON r.id = ur.redeemable_id -WHERE r.event_id = $1 GROUP BY r.id ` -type GetRedeemablesByEventIDRow struct { - ID uuid.UUID `json:"id"` - EventID uuid.UUID `json:"event_id"` - Name string `json:"name"` - TotalStock int32 `json:"total_stock"` - MaxUserAmount int32 `json:"max_user_amount"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - TotalRedeemed interface{} `json:"total_redeemed"` +type GetRedeemablesRow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + TotalStock int32 `json:"total_stock"` + MaxUserAmount int32 `json:"max_user_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + TotalRedeemed interface{} `json:"total_redeemed"` } -// Using event id, return all redeemables associated. Should also return data like how many have been redeemed already -func (q *Queries) GetRedeemablesByEventID(ctx context.Context, eventID uuid.UUID) ([]GetRedeemablesByEventIDRow, error) { - rows, err := q.db.Query(ctx, getRedeemablesByEventID, eventID) +func (q *Queries) GetRedeemables(ctx context.Context) ([]GetRedeemablesRow, error) { + rows, err := q.db.Query(ctx, getRedeemables) if err != nil { return nil, err } defer rows.Close() - items := []GetRedeemablesByEventIDRow{} + items := []GetRedeemablesRow{} for rows.Next() { - var i GetRedeemablesByEventIDRow + var i GetRedeemablesRow if err := rows.Scan( &i.ID, - &i.EventID, &i.Name, &i.TotalStock, &i.MaxUserAmount, @@ -119,16 +112,23 @@ FROM user_redemptions ur WHERE ur.redeemable_id = $1 ` -// Gather all redemption info for a specific reedeemable (who has redeemed already) -func (q *Queries) GetRedemptionInfoByRedeemableID(ctx context.Context, redeemableID uuid.UUID) ([]UserRedemption, error) { +type GetRedemptionInfoByRedeemableIDRow struct { + UserID uuid.UUID `json:"user_id"` + RedeemableID uuid.UUID `json:"redeemable_id"` + Amount int32 `json:"amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (q *Queries) GetRedemptionInfoByRedeemableID(ctx context.Context, redeemableID uuid.UUID) ([]GetRedemptionInfoByRedeemableIDRow, error) { rows, err := q.db.Query(ctx, getRedemptionInfoByRedeemableID, redeemableID) if err != nil { return nil, err } defer rows.Close() - items := []UserRedemption{} + items := []GetRedemptionInfoByRedeemableIDRow{} for rows.Next() { - var i UserRedemption + var i GetRedemptionInfoByRedeemableIDRow if err := rows.Scan( &i.UserID, &i.RedeemableID, @@ -147,7 +147,7 @@ func (q *Queries) GetRedemptionInfoByRedeemableID(ctx context.Context, redeemabl } const redeemRedeemable = `-- name: RedeemRedeemable :one -INSERT INTO user_redemptions (user_id, redeemable_id, amount) +INSERT INTO user_redemptions (user_id, redeemable_id, hackathon_id, amount) SELECT $1, $2, 1 WHERE ( SELECT COALESCE(SUM(amount), 0) @@ -159,7 +159,7 @@ DO UPDATE SET amount = user_redemptions.amount + 1, updated_at = CURRENT_TIMESTAMP WHERE user_redemptions.amount < (SELECT max_user_amount FROM redeemables WHERE id = $2) -RETURNING user_id, redeemable_id, amount, created_at, updated_at +RETURNING user_id, redeemable_id, amount, created_at, updated_at, hackathon_id ` type RedeemRedeemableParams struct { @@ -167,7 +167,6 @@ type RedeemRedeemableParams struct { RedeemableID uuid.UUID `json:"redeemable_id"` } -// Using user id and redeemable id, attempt! to redeem a redeemable func (q *Queries) RedeemRedeemable(ctx context.Context, arg RedeemRedeemableParams) (UserRedemption, error) { row := q.db.QueryRow(ctx, redeemRedeemable, arg.UserID, arg.RedeemableID) var i UserRedemption @@ -177,6 +176,7 @@ func (q *Queries) RedeemRedeemable(ctx context.Context, arg RedeemRedeemablePara &i.Amount, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } @@ -189,7 +189,7 @@ SET max_user_amount = COALESCE($4, max_user_amount), updated_at = CURRENT_TIMESTAMP WHERE id = $1 -RETURNING id, event_id, name, amount, max_user_amount, created_at, updated_at +RETURNING id, name, amount, max_user_amount, created_at, updated_at, hackathon_id ` type UpdateRedeemableParams struct { @@ -209,12 +209,12 @@ func (q *Queries) UpdateRedeemable(ctx context.Context, arg UpdateRedeemablePara var i Redeemable err := row.Scan( &i.ID, - &i.EventID, &i.Name, &i.Amount, &i.MaxUserAmount, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } @@ -232,7 +232,6 @@ type UpdateRedemptionParams struct { RedeemableID uuid.UUID `json:"redeemable_id"` } -// Update a redemption record for a user and redeemable (for removing redemption mostly) func (q *Queries) UpdateRedemption(ctx context.Context, arg UpdateRedemptionParams) error { _, err := q.db.Exec(ctx, updateRedemption, arg.Amount, arg.UserID, arg.RedeemableID) return err diff --git a/apps/api/internal/db/sqlc/sessions.sql.go b/apps/api/internal/database/sqlc/sessions.sql.go similarity index 78% rename from apps/api/internal/db/sqlc/sessions.sql.go rename to apps/api/internal/database/sqlc/sessions.sql.go index c95d1c82..ea2f8f63 100644 --- a/apps/api/internal/db/sqlc/sessions.sql.go +++ b/apps/api/internal/database/sqlc/sessions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: sessions.sql package sqlc @@ -13,7 +13,7 @@ import ( ) const createSession = `-- name: CreateSession :one -INSERT INTO auth.sessions (user_id, expires_at, ip_address, user_agent) +INSERT INTO sessions (user_id, expires_at, ip_address, user_agent) VALUES ($1, $2, $3, $4) RETURNING id, user_id, expires_at, ip_address, user_agent, created_at, updated_at, last_used_at ` @@ -25,14 +25,14 @@ type CreateSessionParams struct { UserAgent *string `json:"user_agent"` } -func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) { +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { row := q.db.QueryRow(ctx, createSession, arg.UserID, arg.ExpiresAt, arg.IpAddress, arg.UserAgent, ) - var i AuthSession + var i Session err := row.Scan( &i.ID, &i.UserID, @@ -47,7 +47,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (A } const deleteExpiredSession = `-- name: DeleteExpiredSession :exec -DELETE FROM auth.sessions +DELETE FROM sessions WHERE expires_at < NOW() ` @@ -57,24 +57,26 @@ func (q *Queries) DeleteExpiredSession(ctx context.Context) error { } const getActiveSessionUserInfo = `-- name: GetActiveSessionUserInfo :one -SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, s.last_used_at -FROM auth.sessions s -JOIN auth.users u ON s.user_id = u.id +SELECT u.id AS user_id, u.name, u.email, u.preferred_email, u.onboarded, u.image, u.role, u.email_consent, u.checked_in_at, u.rfid, s.last_used_at +FROM sessions s +JOIN users u ON s.user_id = u.id WHERE s.id = $1 AND (s.expires_at > NOW()) LIMIT 1 ` type GetActiveSessionUserInfoRow struct { - UserID uuid.UUID `json:"user_id"` - Name string `json:"name"` - Email *string `json:"email"` - PreferredEmail *string `json:"preferred_email"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - Role AuthUserRole `json:"role"` - EmailConsent bool `json:"email_consent"` - LastUsedAt time.Time `json:"last_used_at"` + UserID uuid.UUID `json:"user_id"` + Name string `json:"name"` + Email *string `json:"email"` + PreferredEmail *string `json:"preferred_email"` + Onboarded bool `json:"onboarded"` + Image *string `json:"image"` + Role UserRole `json:"role"` + EmailConsent bool `json:"email_consent"` + CheckedInAt *time.Time `json:"checked_in_at"` + Rfid *string `json:"rfid"` + LastUsedAt time.Time `json:"last_used_at"` } func (q *Queries) GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) { @@ -89,19 +91,21 @@ func (q *Queries) GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (G &i.Image, &i.Role, &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, &i.LastUsedAt, ) return i, err } const getSessionByID = `-- name: GetSessionByID :one -SELECT id, user_id, expires_at, ip_address, user_agent, created_at, updated_at, last_used_at FROM auth.sessions +SELECT id, user_id, expires_at, ip_address, user_agent, created_at, updated_at, last_used_at FROM sessions WHERE id = $1 ` -func (q *Queries) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) { +func (q *Queries) GetSessionByID(ctx context.Context, id uuid.UUID) (Session, error) { row := q.db.QueryRow(ctx, getSessionByID, id) - var i AuthSession + var i Session err := row.Scan( &i.ID, &i.UserID, @@ -116,19 +120,19 @@ func (q *Queries) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession } const getSessionsByUserID = `-- name: GetSessionsByUserID :many -SELECT id, user_id, expires_at, ip_address, user_agent, created_at, updated_at, last_used_at FROM auth.sessions +SELECT id, user_id, expires_at, ip_address, user_agent, created_at, updated_at, last_used_at FROM sessions WHERE user_id = $1 ` -func (q *Queries) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) { +func (q *Queries) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]Session, error) { rows, err := q.db.Query(ctx, getSessionsByUserID, userID) if err != nil { return nil, err } defer rows.Close() - items := []AuthSession{} + items := []Session{} for rows.Next() { - var i AuthSession + var i Session if err := rows.Scan( &i.ID, &i.UserID, @@ -150,7 +154,7 @@ func (q *Queries) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([] } const invalidateSessionByID = `-- name: InvalidateSessionByID :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = NOW() WHERE id = $1 ` @@ -161,7 +165,7 @@ func (q *Queries) InvalidateSessionByID(ctx context.Context, id uuid.UUID) error } const touchSession = `-- name: TouchSession :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = $2, last_used_at = NOW() WHERE id = $1 ` @@ -177,7 +181,7 @@ func (q *Queries) TouchSession(ctx context.Context, arg TouchSessionParams) erro } const updateSessionExpiration = `-- name: UpdateSessionExpiration :exec -UPDATE auth.sessions +UPDATE sessions SET expires_at = $2 WHERE id = $1 ` diff --git a/apps/api/internal/db/sqlc/stats.sql.go b/apps/api/internal/database/sqlc/stats.sql.go similarity index 81% rename from apps/api/internal/db/sqlc/stats.sql.go rename to apps/api/internal/database/sqlc/stats.sql.go index e0e2a279..2703cbfb 100644 --- a/apps/api/internal/db/sqlc/stats.sql.go +++ b/apps/api/internal/database/sqlc/stats.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: stats.sql package sqlc @@ -8,7 +8,6 @@ package sqlc import ( "context" - "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) @@ -23,7 +22,6 @@ SELECT COUNT(*) FILTER (WHERE (application->>'age')::int >= 23) AS age_23_plus FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 ` type GetApplicationAgeSplitRow struct { @@ -36,8 +34,8 @@ type GetApplicationAgeSplitRow struct { Age23Plus int64 `json:"age_23_plus"` } -func (q *Queries) GetApplicationAgeSplit(ctx context.Context, eventID uuid.UUID) (GetApplicationAgeSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationAgeSplit, eventID) +func (q *Queries) GetApplicationAgeSplit(ctx context.Context) (GetApplicationAgeSplitRow, error) { + row := q.db.QueryRow(ctx, getApplicationAgeSplit) var i GetApplicationAgeSplitRow err := row.Scan( &i.Underage, @@ -60,7 +58,6 @@ SELECT COUNT(*) FILTER (WHERE application->>'gender' = '') AS other FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 ` type GetApplicationGenderSplitRow struct { @@ -71,8 +68,8 @@ type GetApplicationGenderSplitRow struct { } // Queries used for statistics, mainly used by overview dashboards etc -func (q *Queries) GetApplicationGenderSplit(ctx context.Context, eventID uuid.UUID) (GetApplicationGenderSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationGenderSplit, eventID) +func (q *Queries) GetApplicationGenderSplit(ctx context.Context) (GetApplicationGenderSplitRow, error) { + row := q.db.QueryRow(ctx, getApplicationGenderSplit) var i GetApplicationGenderSplitRow err := row.Scan( &i.Male, @@ -90,7 +87,6 @@ SELECT FROM applications, LATERAL unnest(string_to_array(application->>'majors', ',')) AS major WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY trim(major) ORDER BY count DESC ` @@ -100,8 +96,8 @@ type GetApplicationMajorSplitRow struct { Count int64 `json:"count"` } -func (q *Queries) GetApplicationMajorSplit(ctx context.Context, eventID uuid.UUID) ([]GetApplicationMajorSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationMajorSplit, eventID) +func (q *Queries) GetApplicationMajorSplit(ctx context.Context) ([]GetApplicationMajorSplitRow, error) { + rows, err := q.db.Query(ctx, getApplicationMajorSplit) if err != nil { return nil, err } @@ -130,7 +126,6 @@ SELECT COUNT(*) AS count FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY CASE WHEN application->>'race' IS NOT NULL AND application->>'race' <> '' THEN application->>'race' @@ -145,8 +140,8 @@ type GetApplicationRaceSplitRow struct { Count int64 `json:"count"` } -func (q *Queries) GetApplicationRaceSplit(ctx context.Context, eventID uuid.UUID) ([]GetApplicationRaceSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationRaceSplit, eventID) +func (q *Queries) GetApplicationRaceSplit(ctx context.Context) ([]GetApplicationRaceSplitRow, error) { + rows, err := q.db.Query(ctx, getApplicationRaceSplit) if err != nil { return nil, err } @@ -171,7 +166,6 @@ SELECT COUNT(*) AS count FROM applications WHERE status <> 'started' AND status IS NOT NULL - AND event_id = $1 GROUP BY (application->>'school')::text ORDER BY count DESC ` @@ -181,8 +175,8 @@ type GetApplicationSchoolSplitRow struct { Count int64 `json:"count"` } -func (q *Queries) GetApplicationSchoolSplit(ctx context.Context, eventID uuid.UUID) ([]GetApplicationSchoolSplitRow, error) { - rows, err := q.db.Query(ctx, getApplicationSchoolSplit, eventID) +func (q *Queries) GetApplicationSchoolSplit(ctx context.Context) ([]GetApplicationSchoolSplitRow, error) { + rows, err := q.db.Query(ctx, getApplicationSchoolSplit) if err != nil { return nil, err } @@ -211,7 +205,6 @@ SELECT COUNT(*) FILTER (WHERE status = 'waitlisted') AS waitlisted, COUNT(*) FILTER (WHERE status = 'withdrawn') AS withdrawn FROM applications -WHERE event_id = $1 ` type GetApplicationStatusSplitRow struct { @@ -224,8 +217,8 @@ type GetApplicationStatusSplitRow struct { Withdrawn int64 `json:"withdrawn"` } -func (q *Queries) GetApplicationStatusSplit(ctx context.Context, eventID uuid.UUID) (GetApplicationStatusSplitRow, error) { - row := q.db.QueryRow(ctx, getApplicationStatusSplit, eventID) +func (q *Queries) GetApplicationStatusSplit(ctx context.Context) (GetApplicationStatusSplitRow, error) { + row := q.db.QueryRow(ctx, getApplicationStatusSplit) var i GetApplicationStatusSplitRow err := row.Scan( &i.Started, @@ -244,7 +237,7 @@ SELECT date_trunc('day', submitted_at AT TIME ZONE 'US/Eastern')::date AS day, COUNT(*) AS count FROM applications -WHERE event_id = $1 AND submitted_at IS NOT NULL +WHERE submitted_at IS NOT NULL GROUP BY day ORDER By day ` @@ -254,8 +247,8 @@ type GetSubmissionTimesRow struct { Count int64 `json:"count"` } -func (q *Queries) GetSubmissionTimes(ctx context.Context, eventID uuid.UUID) ([]GetSubmissionTimesRow, error) { - rows, err := q.db.Query(ctx, getSubmissionTimes, eventID) +func (q *Queries) GetSubmissionTimes(ctx context.Context) ([]GetSubmissionTimesRow, error) { + rows, err := q.db.Query(ctx, getSubmissionTimes) if err != nil { return nil, err } diff --git a/apps/api/internal/db/sqlc/team_invitations.sql.go b/apps/api/internal/database/sqlc/team_invitations.sql.go similarity index 92% rename from apps/api/internal/db/sqlc/team_invitations.sql.go rename to apps/api/internal/database/sqlc/team_invitations.sql.go index 972762b4..be0ae076 100644 --- a/apps/api/internal/db/sqlc/team_invitations.sql.go +++ b/apps/api/internal/database/sqlc/team_invitations.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_invitations.sql package sqlc @@ -125,8 +125,8 @@ ORDER BY created_at DESC ` type ListInvitationsByInvitedUserIDAndStatusParams struct { - InvitedUserID *uuid.UUID `json:"invited_user_id"` - Status InvitationStatus `json:"status"` + InvitedUserID *uuid.UUID `json:"invited_user_id"` + Status interface{} `json:"status"` } func (q *Queries) ListInvitationsByInvitedUserIDAndStatus(ctx context.Context, arg ListInvitationsByInvitedUserIDAndStatusParams) ([]TeamInvitation, error) { @@ -238,13 +238,13 @@ RETURNING id, team_id, invited_by_user_id, invited_email, invited_user_id, statu ` type UpdateInvitationParams struct { - InvitedUserIDDoUpdate bool `json:"invited_user_id_do_update"` - InvitedUserID uuid.UUID `json:"invited_user_id"` - StatusDoUpdate bool `json:"status_do_update"` - Status InvitationStatus `json:"status"` - ExpiresAtDoUpdate bool `json:"expires_at_do_update"` - ExpiresAt time.Time `json:"expires_at"` - ID uuid.UUID `json:"id"` + InvitedUserIDDoUpdate bool `json:"invited_user_id_do_update"` + InvitedUserID uuid.UUID `json:"invited_user_id"` + StatusDoUpdate bool `json:"status_do_update"` + Status interface{} `json:"status"` + ExpiresAtDoUpdate bool `json:"expires_at_do_update"` + ExpiresAt time.Time `json:"expires_at"` + ID uuid.UUID `json:"id"` } func (q *Queries) UpdateInvitation(ctx context.Context, arg UpdateInvitationParams) (TeamInvitation, error) { diff --git a/apps/api/internal/db/sqlc/team_join_requests.sql.go b/apps/api/internal/database/sqlc/team_join_requests.sql.go similarity index 73% rename from apps/api/internal/db/sqlc/team_join_requests.sql.go rename to apps/api/internal/database/sqlc/team_join_requests.sql.go index a81c7d8c..f11b8391 100644 --- a/apps/api/internal/db/sqlc/team_join_requests.sql.go +++ b/apps/api/internal/database/sqlc/team_join_requests.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_join_requests.sql package sqlc @@ -54,7 +54,7 @@ func (q *Queries) DeleteJoinRequest(ctx context.Context, id uuid.UUID) error { return err } -const deleteJoinRequestsByUserAndEventAndStatus = `-- name: DeleteJoinRequestsByUserAndEventAndStatus :exec +const deleteJoinRequestsByUserAndStatus = `-- name: DeleteJoinRequestsByUserAndStatus :exec DELETE FROM team_join_requests tjr WHERE tjr.user_id = $1 AND tjr.status = $2 @@ -62,18 +62,16 @@ WHERE tjr.user_id = $1 SELECT 1 FROM teams t WHERE t.id = tjr.team_id - AND t.event_id = $3 ) ` -type DeleteJoinRequestsByUserAndEventAndStatusParams struct { - UserID uuid.UUID `json:"user_id"` - Status JoinRequestStatus `json:"status"` - EventID *uuid.UUID `json:"event_id"` +type DeleteJoinRequestsByUserAndStatusParams struct { + UserID uuid.UUID `json:"user_id"` + Status TeamJoinRequestStatus `json:"status"` } -func (q *Queries) DeleteJoinRequestsByUserAndEventAndStatus(ctx context.Context, arg DeleteJoinRequestsByUserAndEventAndStatusParams) error { - _, err := q.db.Exec(ctx, deleteJoinRequestsByUserAndEventAndStatus, arg.UserID, arg.Status, arg.EventID) +func (q *Queries) DeleteJoinRequestsByUserAndStatus(ctx context.Context, arg DeleteJoinRequestsByUserAndStatusParams) error { + _, err := q.db.Exec(ctx, deleteJoinRequestsByUserAndStatus, arg.UserID, arg.Status) return err } @@ -106,30 +104,30 @@ SELECT u.name AS user_name, u.image AS user_image FROM team_join_requests tjr -JOIN auth.users u ON u.id = tjr.user_id +JOIN users u ON u.id = tjr.user_id WHERE tjr.team_id = $1::uuid AND tjr.status = $2::join_request_status ORDER BY tjr.created_at DESC ` type ListJoinRequestsByTeamAndStatusWithUserParams struct { - TeamID uuid.UUID `json:"team_id"` - Status JoinRequestStatus `json:"status"` + TeamID uuid.UUID `json:"team_id"` + Status interface{} `json:"status"` } type ListJoinRequestsByTeamAndStatusWithUserRow struct { - ID uuid.UUID `json:"id"` - TeamID uuid.UUID `json:"team_id"` - UserID uuid.UUID `json:"user_id"` - RequestMessage *string `json:"request_message"` - Status JoinRequestStatus `json:"status"` - ProcessedByUserID *uuid.UUID `json:"processed_by_user_id"` - ProcessedAt *time.Time `json:"processed_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - UserEmail *string `json:"user_email"` - UserName string `json:"user_name"` - UserImage *string `json:"user_image"` + ID uuid.UUID `json:"id"` + TeamID uuid.UUID `json:"team_id"` + UserID uuid.UUID `json:"user_id"` + RequestMessage *string `json:"request_message"` + Status TeamJoinRequestStatus `json:"status"` + ProcessedByUserID *uuid.UUID `json:"processed_by_user_id"` + ProcessedAt *time.Time `json:"processed_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserEmail *string `json:"user_email"` + UserName string `json:"user_name"` + UserImage *string `json:"user_image"` } func (q *Queries) ListJoinRequestsByTeamAndStatusWithUser(ctx context.Context, arg ListJoinRequestsByTeamAndStatusWithUserParams) ([]ListJoinRequestsByTeamAndStatusWithUserRow, error) { @@ -173,8 +171,8 @@ ORDER BY created_at DESC ` type ListTeamJoinRequestsByTeamIDAndStatusParams struct { - TeamID uuid.UUID `json:"team_id"` - Status JoinRequestStatus `json:"status"` + TeamID uuid.UUID `json:"team_id"` + Status interface{} `json:"status"` } func (q *Queries) ListTeamJoinRequestsByTeamIDAndStatus(ctx context.Context, arg ListTeamJoinRequestsByTeamIDAndStatusParams) ([]TeamJoinRequest, error) { @@ -207,7 +205,7 @@ func (q *Queries) ListTeamJoinRequestsByTeamIDAndStatus(ctx context.Context, arg return items, nil } -const listTeamJoinRequestsByUserAndEventAndStatus = `-- name: ListTeamJoinRequestsByUserAndEventAndStatus :many +const listTeamJoinRequestsByUserAndStatus = `-- name: ListTeamJoinRequestsByUserAndStatus :many SELECT tjr.id, tjr.team_id, tjr.user_id, tjr.request_message, tjr.status, tjr.processed_by_user_id, tjr.processed_at, tjr.created_at, tjr.updated_at FROM team_join_requests tjr WHERE tjr.user_id = $1 @@ -216,19 +214,17 @@ WHERE tjr.user_id = $1 SELECT 1 FROM teams t WHERE t.id = tjr.team_id - AND t.event_id = $3 ) ORDER BY tjr.created_at DESC ` -type ListTeamJoinRequestsByUserAndEventAndStatusParams struct { - UserID uuid.UUID `json:"user_id"` - Status JoinRequestStatus `json:"status"` - EventID *uuid.UUID `json:"event_id"` +type ListTeamJoinRequestsByUserAndStatusParams struct { + UserID uuid.UUID `json:"user_id"` + Status TeamJoinRequestStatus `json:"status"` } -func (q *Queries) ListTeamJoinRequestsByUserAndEventAndStatus(ctx context.Context, arg ListTeamJoinRequestsByUserAndEventAndStatusParams) ([]TeamJoinRequest, error) { - rows, err := q.db.Query(ctx, listTeamJoinRequestsByUserAndEventAndStatus, arg.UserID, arg.Status, arg.EventID) +func (q *Queries) ListTeamJoinRequestsByUserAndStatus(ctx context.Context, arg ListTeamJoinRequestsByUserAndStatusParams) ([]TeamJoinRequest, error) { + rows, err := q.db.Query(ctx, listTeamJoinRequestsByUserAndStatus, arg.UserID, arg.Status) if err != nil { return nil, err } @@ -308,15 +304,15 @@ RETURNING id, team_id, user_id, request_message, status, processed_by_user_id, p ` type UpdateTeamJoinRequestParams struct { - RequestMessageDoUpdate bool `json:"request_message_do_update"` - RequestMessage *string `json:"request_message"` - StatusDoUpdate bool `json:"status_do_update"` - Status JoinRequestStatus `json:"status"` - ProcessedByUserIDDoUpdate bool `json:"processed_by_user_id_do_update"` - ProcessedByUserID uuid.UUID `json:"processed_by_user_id"` - ProcessedAtDoUpdate bool `json:"processed_at_do_update"` - ProcessedAt time.Time `json:"processed_at"` - ID uuid.UUID `json:"id"` + RequestMessageDoUpdate bool `json:"request_message_do_update"` + RequestMessage *string `json:"request_message"` + StatusDoUpdate bool `json:"status_do_update"` + Status interface{} `json:"status"` + ProcessedByUserIDDoUpdate bool `json:"processed_by_user_id_do_update"` + ProcessedByUserID uuid.UUID `json:"processed_by_user_id"` + ProcessedAtDoUpdate bool `json:"processed_at_do_update"` + ProcessedAt time.Time `json:"processed_at"` + ID uuid.UUID `json:"id"` } func (q *Queries) UpdateTeamJoinRequest(ctx context.Context, arg UpdateTeamJoinRequestParams) (TeamJoinRequest, error) { diff --git a/apps/api/internal/db/sqlc/team_members.sql.go b/apps/api/internal/database/sqlc/team_members.sql.go similarity index 79% rename from apps/api/internal/db/sqlc/team_members.sql.go rename to apps/api/internal/database/sqlc/team_members.sql.go index 5ecfaf60..c9be1e57 100644 --- a/apps/api/internal/db/sqlc/team_members.sql.go +++ b/apps/api/internal/database/sqlc/team_members.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_members.sql package sqlc @@ -53,22 +53,16 @@ func (q *Queries) CreateTeamMember(ctx context.Context, arg CreateTeamMemberPara return i, err } -const getTeamMemberByUserAndEvent = `-- name: GetTeamMemberByUserAndEvent :one +const getTeamMemberByUserId = `-- name: GetTeamMemberByUserId :one SELECT tm.user_id, tm.team_id, tm.joined_at FROM team_members tm JOIN teams t on tm.team_id = t.id WHERE tm.user_id = $1 - AND t.event_id = $2 LIMIT 1 ` -type GetTeamMemberByUserAndEventParams struct { - UserID uuid.UUID `json:"user_id"` - EventID *uuid.UUID `json:"event_id"` -} - -func (q *Queries) GetTeamMemberByUserAndEvent(ctx context.Context, arg GetTeamMemberByUserAndEventParams) (TeamMember, error) { - row := q.db.QueryRow(ctx, getTeamMemberByUserAndEvent, arg.UserID, arg.EventID) +func (q *Queries) GetTeamMemberByUserId(ctx context.Context, userID uuid.UUID) (TeamMember, error) { + row := q.db.QueryRow(ctx, getTeamMemberByUserId, userID) var i TeamMember err := row.Scan(&i.UserID, &i.TeamID, &i.JoinedAt) return i, err @@ -84,17 +78,17 @@ SELECT FROM team_members tm JOIN - auth.users u ON tm.user_id = u.id + users u ON tm.user_id = u.id WHERE tm.team_id = $1 ` type GetTeamMembersRow struct { - UserID uuid.UUID `json:"user_id"` - Email *string `json:"email"` - Image *string `json:"image"` - Name string `json:"name"` - JoinedAt *time.Time `json:"joined_at"` + UserID uuid.UUID `json:"user_id"` + Email *string `json:"email"` + Image *string `json:"image"` + Name string `json:"name"` + JoinedAt time.Time `json:"joined_at"` } func (q *Queries) GetTeamMembers(ctx context.Context, teamID uuid.UUID) ([]GetTeamMembersRow, error) { diff --git a/apps/api/internal/db/sqlc/teams.sql.go b/apps/api/internal/database/sqlc/teams.sql.go similarity index 64% rename from apps/api/internal/db/sqlc/teams.sql.go rename to apps/api/internal/database/sqlc/teams.sql.go index 1ae5249a..911db0ff 100644 --- a/apps/api/internal/db/sqlc/teams.sql.go +++ b/apps/api/internal/database/sqlc/teams.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: teams.sql package sqlc @@ -15,31 +15,31 @@ const createTeam = `-- name: CreateTeam :one INSERT INTO teams ( name, owner_id, - event_id + hackathon_id ) VALUES ( $1, $2, $3 ) -RETURNING id, name, owner_id, event_id, created_at, updated_at +RETURNING id, name, owner_id, created_at, updated_at, hackathon_id ` type CreateTeamParams struct { - Name string `json:"name"` - OwnerID *uuid.UUID `json:"owner_id"` - EventID *uuid.UUID `json:"event_id"` + Name string `json:"name"` + OwnerID *uuid.UUID `json:"owner_id"` + HackathonID string `json:"hackathon_id"` } func (q *Queries) CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error) { - row := q.db.QueryRow(ctx, createTeam, arg.Name, arg.OwnerID, arg.EventID) + row := q.db.QueryRow(ctx, createTeam, arg.Name, arg.OwnerID, arg.HackathonID) var i Team err := row.Scan( &i.ID, &i.Name, &i.OwnerID, - &i.EventID, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } @@ -55,7 +55,7 @@ func (q *Queries) DeleteTeam(ctx context.Context, id uuid.UUID) error { } const getTeamById = `-- name: GetTeamById :one -SELECT id, name, owner_id, event_id, created_at, updated_at +SELECT id, name, owner_id, created_at, updated_at, hackathon_id FROM teams WHERE id = $1 ` @@ -67,59 +67,44 @@ func (q *Queries) GetTeamById(ctx context.Context, id uuid.UUID) (Team, error) { &i.ID, &i.Name, &i.OwnerID, - &i.EventID, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } -const getUserEventTeam = `-- name: GetUserEventTeam :one +const getUserTeam = `-- name: GetUserTeam :one SELECT t.id, t.name, - t.owner_id, - t.event_id + t.owner_id FROM teams t JOIN team_members tm ON t.id = tm.team_id -WHERE - t.event_id = $1 - AND tm.user_id = $2 +WHERE tm.user_id = $1 LIMIT 1 ` -type GetUserEventTeamParams struct { - EventID *uuid.UUID `json:"event_id"` - UserID uuid.UUID `json:"user_id"` -} - -type GetUserEventTeamRow struct { +type GetUserTeamRow struct { ID uuid.UUID `json:"id"` Name string `json:"name"` OwnerID *uuid.UUID `json:"owner_id"` - EventID *uuid.UUID `json:"event_id"` } -func (q *Queries) GetUserEventTeam(ctx context.Context, arg GetUserEventTeamParams) (GetUserEventTeamRow, error) { - row := q.db.QueryRow(ctx, getUserEventTeam, arg.EventID, arg.UserID) - var i GetUserEventTeamRow - err := row.Scan( - &i.ID, - &i.Name, - &i.OwnerID, - &i.EventID, - ) +func (q *Queries) GetUserTeam(ctx context.Context, userID uuid.UUID) (GetUserTeamRow, error) { + row := q.db.QueryRow(ctx, getUserTeam, userID) + var i GetUserTeamRow + err := row.Scan(&i.ID, &i.Name, &i.OwnerID) return i, err } -const listTeamsWithMembersByEvent = `-- name: ListTeamsWithMembersByEvent :many +const listTeamsWithMembers = `-- name: ListTeamsWithMembers :many SELECT t.id, t.name, t.owner_id, - t.event_id, -- Step 1: Cast the aggregated JSON array to JSONB (COALESCE( json_agg( @@ -138,45 +123,40 @@ FROM LEFT JOIN team_members tm ON t.id = tm.team_id LEFT JOIN - auth.users u ON tm.user_id = u.id -WHERE - t.event_id = $1 + users u ON tm.user_id = u.id GROUP BY t.id ORDER BY t.created_at DESC -LIMIT $2 -OFFSET $3 +LIMIT $1 +OFFSET $2 ` -type ListTeamsWithMembersByEventParams struct { - EventID *uuid.UUID `json:"event_id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` +type ListTeamsWithMembersParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` } -type ListTeamsWithMembersByEventRow struct { +type ListTeamsWithMembersRow struct { ID uuid.UUID `json:"id"` Name string `json:"name"` OwnerID *uuid.UUID `json:"owner_id"` - EventID *uuid.UUID `json:"event_id"` Members []byte `json:"members"` } -func (q *Queries) ListTeamsWithMembersByEvent(ctx context.Context, arg ListTeamsWithMembersByEventParams) ([]ListTeamsWithMembersByEventRow, error) { - rows, err := q.db.Query(ctx, listTeamsWithMembersByEvent, arg.EventID, arg.Limit, arg.Offset) +func (q *Queries) ListTeamsWithMembers(ctx context.Context, arg ListTeamsWithMembersParams) ([]ListTeamsWithMembersRow, error) { + rows, err := q.db.Query(ctx, listTeamsWithMembers, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() - items := []ListTeamsWithMembersByEventRow{} + items := []ListTeamsWithMembersRow{} for rows.Next() { - var i ListTeamsWithMembersByEventRow + var i ListTeamsWithMembersRow if err := rows.Scan( &i.ID, &i.Name, &i.OwnerID, - &i.EventID, &i.Members, ); err != nil { return nil, err @@ -196,7 +176,7 @@ SET name = CASE WHEN $3::boolean THEN $4 ELSE name END WHERE id = $5::uuid -RETURNING id, name, owner_id, event_id, created_at, updated_at +RETURNING id, name, owner_id, created_at, updated_at, hackathon_id ` type UpdateTeamByIdParams struct { @@ -220,9 +200,9 @@ func (q *Queries) UpdateTeamById(ctx context.Context, arg UpdateTeamByIdParams) &i.ID, &i.Name, &i.OwnerID, - &i.EventID, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, ) return i, err } diff --git a/apps/api/internal/db/sqlc/users.sql.go b/apps/api/internal/database/sqlc/users.sql.go similarity index 51% rename from apps/api/internal/db/sqlc/users.sql.go rename to apps/api/internal/database/sqlc/users.sql.go index 2798f1b5..5464419b 100644 --- a/apps/api/internal/db/sqlc/users.sql.go +++ b/apps/api/internal/database/sqlc/users.sql.go @@ -1,20 +1,21 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: users.sql package sqlc import ( "context" + "time" "github.com/google/uuid" ) const createUser = `-- name: CreateUser :one -INSERT INTO auth.users (name, email, image) +INSERT INTO users (name, email, image) VALUES ($1, $2, $3) -RETURNING id, name, email, email_verified, onboarded, image, created_at, updated_at, role, preferred_email, email_consent +RETURNING id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role ` type CreateUserParams struct { @@ -23,9 +24,9 @@ type CreateUserParams struct { Image *string `json:"image"` } -func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) { +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { row := q.db.QueryRow(ctx, createUser, arg.Name, arg.Email, arg.Image) - var i AuthUser + var i User err := row.Scan( &i.ID, &i.Name, @@ -35,15 +36,18 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUse &i.Image, &i.CreatedAt, &i.UpdatedAt, - &i.Role, &i.PreferredEmail, &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, + &i.Role, ) return i, err } const deleteUser = `-- name: DeleteUser :exec -DELETE FROM auth.users +DELETE FROM users WHERE id = $1 ` @@ -53,13 +57,13 @@ func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error { } const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, role, preferred_email, email_consent FROM auth.users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users WHERE email = $1 ` -func (q *Queries) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) { +func (q *Queries) GetUserByEmail(ctx context.Context, email *string) (User, error) { row := q.db.QueryRow(ctx, getUserByEmail, email) - var i AuthUser + var i User err := row.Scan( &i.ID, &i.Name, @@ -69,21 +73,24 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email *string) (AuthUser, &i.Image, &i.CreatedAt, &i.UpdatedAt, - &i.Role, &i.PreferredEmail, &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, + &i.Role, ) return i, err } const getUserByID = `-- name: GetUserByID :one -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, role, preferred_email, email_consent FROM auth.users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users WHERE id = $1 ` -func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) { +func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) { row := q.db.QueryRow(ctx, getUserByID, id) - var i AuthUser + var i User err := row.Scan( &i.ID, &i.Name, @@ -93,9 +100,39 @@ func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, erro &i.Image, &i.CreatedAt, &i.UpdatedAt, + &i.PreferredEmail, + &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, &i.Role, + ) + return i, err +} + +const getUserByRFID = `-- name: GetUserByRFID :one +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role FROM users +WHERE rfid = $1 +` + +func (q *Queries) GetUserByRFID(ctx context.Context, rfid *string) (User, error) { + row := q.db.QueryRow(ctx, getUserByRFID, rfid) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.EmailVerified, + &i.Onboarded, + &i.Image, + &i.CreatedAt, + &i.UpdatedAt, &i.PreferredEmail, &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, + &i.Role, ) return i, err } @@ -109,7 +146,7 @@ SELECT WHEN preferred_email IS NOT NULL AND preferred_email != '' THEN preferred_email ELSE email END AS contact_email -FROM auth.users +FROM users WHERE id = $1 ` @@ -133,8 +170,8 @@ func (q *Queries) GetUserEmailInfoById(ctx context.Context, id uuid.UUID) (GetUs } const getUsers = `-- name: GetUsers :many -SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, role, preferred_email, email_consent -FROM auth.users +SELECT id, name, email, email_verified, onboarded, image, created_at, updated_at, preferred_email, email_consent, checked_in_at, rfid, role_assigned_at, role +FROM users WHERE LOWER(name) LIKE LOWER('%' || COALESCE($1, '') || '%') OR LOWER(email) LIKE LOWER('%' || COALESCE($1, '') || '%') ORDER BY name @@ -147,15 +184,15 @@ type GetUsersParams struct { Limit int32 `json:"limit"` } -func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]AuthUser, error) { +func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error) { rows, err := q.db.Query(ctx, getUsers, arg.Search, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() - items := []AuthUser{} + items := []User{} for rows.Next() { - var i AuthUser + var i User if err := rows.Scan( &i.ID, &i.Name, @@ -165,9 +202,12 @@ func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]AuthUser, &i.Image, &i.CreatedAt, &i.UpdatedAt, - &i.Role, &i.PreferredEmail, &i.EmailConsent, + &i.CheckedInAt, + &i.Rfid, + &i.RoleAssignedAt, + &i.Role, ); err != nil { return nil, err } @@ -179,8 +219,69 @@ func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]AuthUser, return items, nil } +const removeRole = `-- name: RemoveRole :exec +UPDATE users +SET role = NULL, + role_assigned_at = NOW() +WHERE id = $1::uuid +` + +func (q *Queries) RemoveRole(ctx context.Context, userID uuid.UUID) error { + _, err := q.db.Exec(ctx, removeRole, userID) + return err +} + +const updateCheckInTime = `-- name: UpdateCheckInTime :exec +UPDATE users +SET checked_in_at = $1 +WHERE id = $2::uuid +` + +type UpdateCheckInTimeParams struct { + CheckedInAt *time.Time `json:"checked_in_at"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) UpdateCheckInTime(ctx context.Context, arg UpdateCheckInTimeParams) error { + _, err := q.db.Exec(ctx, updateCheckInTime, arg.CheckedInAt, arg.UserID) + return err +} + +const updateRFID = `-- name: UpdateRFID :exec +UPDATE users +SET rfid = $1 +WHERE id = $2::uuid +` + +type UpdateRFIDParams struct { + Rfid *string `json:"rfid"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) UpdateRFID(ctx context.Context, arg UpdateRFIDParams) error { + _, err := q.db.Exec(ctx, updateRFID, arg.Rfid, arg.UserID) + return err +} + +const updateRole = `-- name: UpdateRole :exec +UPDATE users +SET role = $1::role_type, + role_assigned_at = NOW() +WHERE id = $2::uuid +` + +type UpdateRoleParams struct { + Role interface{} `json:"role"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) UpdateRole(ctx context.Context, arg UpdateRoleParams) error { + _, err := q.db.Exec(ctx, updateRole, arg.Role, arg.UserID) + return err +} + const updateUser = `-- name: UpdateUser :exec -UPDATE auth.users +UPDATE users SET name = CASE WHEN $1::boolean THEN $2 ELSE name END, email = CASE WHEN $3::boolean THEN $4 ELSE email END, @@ -189,27 +290,37 @@ SET onboarded = CASE WHEN $9::boolean THEN $10 ELSE onboarded END, image = CASE WHEN $11::boolean THEN $12 ELSE image END, email_consent = CASE WHEN $13::boolean THEN $14 ELSE email_consent END, + checked_in_at = CASE WHEN $15::boolean THEN $16 ELSE checked_in_at END, + rfid = CASE WHEN $17::boolean THEN $18 ELSE rfid END, + role = CASE WHEN $19::boolean THEN $20 ELSE role END, + role_assigned_at = CASE WHEN $19::boolean THEN NOW() ELSE role_assigned_at END, updated_at = NOW() WHERE - id = $15::uuid + id = $21::uuid ` type UpdateUserParams struct { - NameDoUpdate bool `json:"name_do_update"` - Name string `json:"name"` - EmailDoUpdate bool `json:"email_do_update"` - Email *string `json:"email"` - EmailVerifiedDoUpdate bool `json:"email_verified_do_update"` - EmailVerified bool `json:"email_verified"` - PreferredEmailDoUpdate bool `json:"preferred_email_do_update"` - PreferredEmail *string `json:"preferred_email"` - OnboardedDoUpdate bool `json:"onboarded_do_update"` - Onboarded bool `json:"onboarded"` - ImageDoUpdate bool `json:"image_do_update"` - Image *string `json:"image"` - EmailConsentDoUpdate bool `json:"email_consent_do_update"` - EmailConsent bool `json:"email_consent"` - ID uuid.UUID `json:"id"` + NameDoUpdate bool `json:"name_do_update"` + Name string `json:"name"` + EmailDoUpdate bool `json:"email_do_update"` + Email *string `json:"email"` + EmailVerifiedDoUpdate bool `json:"email_verified_do_update"` + EmailVerified bool `json:"email_verified"` + PreferredEmailDoUpdate bool `json:"preferred_email_do_update"` + PreferredEmail *string `json:"preferred_email"` + OnboardedDoUpdate bool `json:"onboarded_do_update"` + Onboarded bool `json:"onboarded"` + ImageDoUpdate bool `json:"image_do_update"` + Image *string `json:"image"` + EmailConsentDoUpdate bool `json:"email_consent_do_update"` + EmailConsent bool `json:"email_consent"` + CheckedInAtDoUpdate bool `json:"checked_in_at_do_update"` + CheckedInAt *time.Time `json:"checked_in_at"` + RfidDoUpdate bool `json:"rfid_do_update"` + Rfid *string `json:"rfid"` + RoleDoUpdate bool `json:"role_do_update"` + Role UserRole `json:"role"` + ID uuid.UUID `json:"id"` } func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { @@ -228,13 +339,19 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { arg.Image, arg.EmailConsentDoUpdate, arg.EmailConsent, + arg.CheckedInAtDoUpdate, + arg.CheckedInAt, + arg.RfidDoUpdate, + arg.Rfid, + arg.RoleDoUpdate, + arg.Role, arg.ID, ) return err } const updateUserOnboarded = `-- name: UpdateUserOnboarded :exec -UPDATE auth.users +UPDATE users SET onboarded = TRUE WHERE id = $1 ` diff --git a/apps/api/internal/db/transaction.go b/apps/api/internal/database/transaction.go similarity index 98% rename from apps/api/internal/db/transaction.go rename to apps/api/internal/database/transaction.go index 163d2a1d..133c9bdb 100644 --- a/apps/api/internal/db/transaction.go +++ b/apps/api/internal/database/transaction.go @@ -1,4 +1,4 @@ -package db +package database import ( "context" diff --git a/apps/api/internal/db/errors.go b/apps/api/internal/db/errors.go deleted file mode 100644 index 73684e7b..00000000 --- a/apps/api/internal/db/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package db - -import ( - "errors" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" -) - -func IsUniqueViolation(err error) bool { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - return pgErr.Code == "23505" - } - return false -} - -func IsNotFound(err error) bool { - return errors.Is(err, pgx.ErrNoRows) -} diff --git a/apps/api/internal/db/migrations/20250512145328_auth_init.sql b/apps/api/internal/db/migrations/20250512145328_auth_init.sql deleted file mode 100644 index 23acf7e6..00000000 --- a/apps/api/internal/db/migrations/20250512145328_auth_init.sql +++ /dev/null @@ -1,105 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE SCHEMA IF NOT EXISTS auth; - -CREATE TABLE auth.users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - email TEXT UNIQUE NOT NULL, - email_verified BOOLEAN NOT NULL DEFAULT FALSE, - onboarded BOOLEAN NOT NULL DEFAULT FALSE, - image TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE auth.sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - token TEXT UNIQUE NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - ip_address TEXT, - user_agent TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TABLE auth.accounts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - provider_id TEXT NOT NULL, - account_id TEXT NOT NULL, - hashed_password TEXT, - access_token TEXT, - refresh_token TEXT, - id_token TEXT, - access_token_expires_at TIMESTAMPTZ, - refresh_token_expires_at TIMESTAMPTZ, - scope TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (provider_id, account_id) -); - --- Index for quick lookup of sessions by user -CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions (user_id); - --- Index for cleaning up expired sessions quickly -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions (expires_at); - --- Index for quick lookup of accounts by user -CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON auth.accounts (user_id); - --- Index for querying provider + account -CREATE INDEX IF NOT EXISTS idx_accounts_provider_account ON auth.accounts (provider_id, account_id); - --- Updated at function -CREATE OR REPLACE FUNCTION update_modified_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = clock_timestamp(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create trigger for users -CREATE TRIGGER set_updated_at_users -BEFORE UPDATE ON auth.users -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- Create trigger for sessions -CREATE TRIGGER set_updated_at_sessions -BEFORE UPDATE ON auth.sessions -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- Create trigger for accounts -CREATE TRIGGER set_updated_at_accounts -BEFORE UPDATE ON auth.accounts -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_users; -DROP TRIGGER IF EXISTS set_updated_at_sessions; -DROP TRIGGER IF EXISTS set_updated_at_accounts; - -DROP FUNCTION IF EXISTS update_modified_column; - - -DROP INDEX IF EXISTS auth.idx_accounts_provider_account; -DROP INDEX IF EXISTS auth.idx_accounts_user_id; - -DROP INDEX IF EXISTS auth.idx_sessions_expires_at; -DROP INDEX IF EXISTS auth.idx_sessions_user_id; - -DROP TABLE IF EXISTS auth.accounts; -DROP TABLE IF EXISTS auth.sessions; -DROP TABLE IF EXISTS auth.users; - -DROP SCHEMA IF EXISTS auth; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250608015747_remove_session_token_and_update_users.sql b/apps/api/internal/db/migrations/20250608015747_remove_session_token_and_update_users.sql deleted file mode 100644 index fb38076f..00000000 --- a/apps/api/internal/db/migrations/20250608015747_remove_session_token_and_update_users.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE auth.sessions DROP COLUMN IF EXISTS token; - -ALTER TABLE auth.users ALTER COLUMN email DROP NOT NULL; - -ALTER TABLE auth.sessions ADD COLUMN last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE auth.sessions ADD COLUMN token TEXT UNIQUE; - -ALTER TABLE auth.users ALTER COLUMN email SET NOT NULL; - -ALTER TABLE auth.sessions DROP COLUMN IF EXISTS last_used_at; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250608194039_add_roles_to_auth_users.sql b/apps/api/internal/db/migrations/20250608194039_add_roles_to_auth_users.sql deleted file mode 100644 index 8eb54915..00000000 --- a/apps/api/internal/db/migrations/20250608194039_add_roles_to_auth_users.sql +++ /dev/null @@ -1,12 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TYPE auth_user_role AS ENUM ('user', 'superuser'); - -ALTER TABLE auth.users ADD COLUMN role auth_user_role NOT NULL DEFAULT 'user'; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE auth.users DROP COLUMN IF EXISTS role; -DROP TYPE IF EXISTS auth_user_role; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250619161938_event_schema.sql b/apps/api/internal/db/migrations/20250619161938_event_schema.sql deleted file mode 100644 index 74d19609..00000000 --- a/apps/api/internal/db/migrations/20250619161938_event_schema.sql +++ /dev/null @@ -1,57 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE events ( - -- IMPORTANT: Double check INSERT queries after making changes to default values here, since any optional parameters must have its default value match. - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - location TEXT, - location_url TEXT, - max_attendees INT, - - -- Application phases - application_open TIMESTAMPTZ NOT NULL, - application_close TIMESTAMPTZ NOT NULL, - rsvp_deadline TIMESTAMPTZ, - decision_release TIMESTAMPTZ, - - -- Event phase - start_time TIMESTAMPTZ NOT NULL, - end_time TIMESTAMPTZ NOT NULL, - - -- Metadata - website_url TEXT, - is_published BOOLEAN DEFAULT FALSE, - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TYPE event_role_type AS ENUM ('admin', 'staff', 'attendee', 'applicant'); - -CREATE TABLE event_roles ( - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - event_id UUID REFERENCES events(id) ON DELETE CASCADE, - role event_role_type NOT NULL, - assigned_at TIMESTAMPTZ DEFAULT NOW(), - - PRIMARY KEY (user_id, event_id) -); - --- Triggers for update_at -CREATE TRIGGER set_updated_at_events -BEFORE UPDATE ON events -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - -DROP TRIGGER IF EXISTS set_updated_at_events ON events; -DROP TABLE IF EXISTS event_roles; -DROP TYPE IF EXISTS event_role_type; -DROP TABLE IF EXISTS events; - --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250621222955_create_applications.sql b/apps/api/internal/db/migrations/20250621222955_create_applications.sql deleted file mode 100644 index ef55c0bc..00000000 --- a/apps/api/internal/db/migrations/20250621222955_create_applications.sql +++ /dev/null @@ -1,39 +0,0 @@ --- +goose Up --- +goose StatementBegin - -CREATE TYPE application_status AS ENUM ('started', 'submitted', 'under_review', 'accepted', 'rejected', 'waitlisted', 'withdrawn'); - -CREATE TABLE applications ( - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - event_id UUID REFERENCES events(id) ON DELETE CASCADE, - -- event_id UUID, - status application_status DEFAULT 'started', - application JSONB NOT NULL DEFAULT '{}'::JSONB, - resume_url TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - saved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - PRIMARY KEY (user_id, event_id) -); - -CREATE INDEX idx_applications_status ON applications(status); -CREATE INDEX idx_applications_event_id ON applications(event_id); - --- Create trigger to update application updates -CREATE TRIGGER set_updated_at_applications -BEFORE UPDATE ON applications -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - - -DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; -DROP TABLE IF EXISTS applications; -DROP TYPE IF EXISTS application_status; - --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql b/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql deleted file mode 100644 index ad958756..00000000 --- a/apps/api/internal/db/migrations/20250627064521_create_mailing_list.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose Up -CREATE TABLE event_interest_submissions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, - email TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - source TEXT -); - -CREATE INDEX idx_event_interest_event_id ON event_interest_submissions (event_id); -CREATE UNIQUE INDEX uniq_event_email ON event_interest_submissions (event_id, email); - --- +goose Down -DROP INDEX IF EXISTS uniq_event_email; -DROP INDEX IF EXISTS idx_event_interest_event_id; -DROP TABLE IF EXISTS event_interest_submissions; diff --git a/apps/api/internal/db/migrations/20250825013123_remove_resume_url_column.sql b/apps/api/internal/db/migrations/20250825013123_remove_resume_url_column.sql deleted file mode 100644 index 8465bf5b..00000000 --- a/apps/api/internal/db/migrations/20250825013123_remove_resume_url_column.sql +++ /dev/null @@ -1,9 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE applications DROP COLUMN IF EXISTS resume_url; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE applications ADD COLUMN resume_url TEXT; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250825033231_update_application_trigger.sql b/apps/api/internal/db/migrations/20250825033231_update_application_trigger.sql deleted file mode 100644 index 254f6a1f..00000000 --- a/apps/api/internal/db/migrations/20250825033231_update_application_trigger.sql +++ /dev/null @@ -1,27 +0,0 @@ --- +goose Up --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; - - -CREATE OR REPLACE FUNCTION update_application_modified_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = clock_timestamp(); - NEW.saved_at = clock_timestamp(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create trigger to update application updates -CREATE TRIGGER set_updated_at_applications -BEFORE UPDATE ON applications -FOR EACH ROW -EXECUTE FUNCTION update_application_modified_column(); - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; -DROP FUNCTION IF EXISTS update_application_modified_column; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250905210705_add_preferred_email_column.sql b/apps/api/internal/db/migrations/20250905210705_add_preferred_email_column.sql deleted file mode 100644 index 732c0c76..00000000 --- a/apps/api/internal/db/migrations/20250905210705_add_preferred_email_column.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE auth.users -ADD COLUMN preferred_email TEXT; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE auth.users -DROP COLUMN preferred_email; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250908055118_add_email_consent_column.sql b/apps/api/internal/db/migrations/20250908055118_add_email_consent_column.sql deleted file mode 100644 index 7aeed502..00000000 --- a/apps/api/internal/db/migrations/20250908055118_add_email_consent_column.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE auth.users -ADD COLUMN email_consent BOOLEAN NOT NULL DEFAULT FALSE; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE auth.users -DROP COLUMN email_consent; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20250917044049_add_event_banner.sql b/apps/api/internal/db/migrations/20250917044049_add_event_banner.sql deleted file mode 100644 index c447fbf6..00000000 --- a/apps/api/internal/db/migrations/20250917044049_add_event_banner.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE events -ADD COLUMN banner TEXT; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE events -DROP COLUMN banner; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251002000347_add_get_event_scope_type.sql b/apps/api/internal/db/migrations/20251002000347_add_get_event_scope_type.sql deleted file mode 100644 index 40a93db5..00000000 --- a/apps/api/internal/db/migrations/20251002000347_add_get_event_scope_type.sql +++ /dev/null @@ -1,13 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TYPE get_event_scope_type AS ENUM ( - 'published', - 'scoped', - 'all' -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TYPE get_event_scope_type; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251022032904_submitted_by_column_applications.sql b/apps/api/internal/db/migrations/20251022032904_submitted_by_column_applications.sql deleted file mode 100644 index 2451b3fc..00000000 --- a/apps/api/internal/db/migrations/20251022032904_submitted_by_column_applications.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE applications -ADD COLUMN submitted_at TIMESTAMPTZ; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE applications -DROP COLUMN submitted_at; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251101211753_teams_table.sql b/apps/api/internal/db/migrations/20251101211753_teams_table.sql deleted file mode 100644 index 0dc3b973..00000000 --- a/apps/api/internal/db/migrations/20251101211753_teams_table.sql +++ /dev/null @@ -1,32 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE teams ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - owner_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, - event_id UUID REFERENCES events(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TRIGGER set_updated_at_teams -BEFORE UPDATE ON auth.users -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - -CREATE TABLE team_members ( - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - joined_at TIMESTAMPTZ DEFAULT NOW(), - - PRIMARY KEY (user_id, team_id) -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - -DROP TABLE IF EXISTS team_members; -DROP TRIGGER IF EXISTS set_updated_at_teams ON auth.users; -DROP TABLE IF EXISTS teams; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251106160823_saved_at_trigger.sql b/apps/api/internal/db/migrations/20251106160823_saved_at_trigger.sql deleted file mode 100644 index e9388283..00000000 --- a/apps/api/internal/db/migrations/20251106160823_saved_at_trigger.sql +++ /dev/null @@ -1,31 +0,0 @@ --- +goose Up --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; -DROP FUNCTION IF EXISTS update_application_modified_column; - -CREATE TRIGGER set_updated_at_applications -BEFORE UPDATE ON applications -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_applications ON applications; - -CREATE OR REPLACE FUNCTION update_application_modified_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = clock_timestamp(); - NEW.saved_at = clock_timestamp(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create trigger to update application updates -CREATE TRIGGER set_updated_at_applications -BEFORE UPDATE ON applications -FOR EACH ROW -EXECUTE FUNCTION update_application_modified_column(); - --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251111212010_invitations_and_join_requests.sql b/apps/api/internal/db/migrations/20251111212010_invitations_and_join_requests.sql deleted file mode 100644 index fbca94de..00000000 --- a/apps/api/internal/db/migrations/20251111212010_invitations_and_join_requests.sql +++ /dev/null @@ -1,58 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TYPE invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REJECTED'); -CREATE TYPE join_request_status AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); - -CREATE TABLE team_invitations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, - invited_by_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - invited_email TEXT NOT NULL, - invited_user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - status invitation_status NOT NULL DEFAULT 'PENDING', - expires_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TRIGGER set_updated_at_team_invitations -BEFORE UPDATE ON team_invitations -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - -CREATE TABLE team_join_requests ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - request_message TEXT, - status join_request_status NOT NULL DEFAULT 'PENDING', - processed_by_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, - processed_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE TRIGGER set_updated_at_team_join_requests -BEFORE UPDATE ON team_join_requests -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - --- Ensure a user can have only one pending invitation per team -CREATE UNIQUE INDEX idx_unique_pending_request -ON team_join_requests (team_id, user_id) -WHERE status = 'PENDING'; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TRIGGER IF EXISTS set_updated_at_team_invitations ON team_invitations; -DROP TRIGGER IF EXISTS set_updated_at_team_join_requests ON team_join_requests; - -DROP INDEX IF EXISTS idx_unique_pending_request; - -DROP TABLE IF EXISTS team_invitations; -DROP TABLE IF EXISTS team_join_requests; - -DROP TYPE IF EXISTS invitation_status; -DROP TYPE IF EXISTS join_request_status; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251121165836_add_app_review_columns.sql b/apps/api/internal/db/migrations/20251121165836_add_app_review_columns.sql deleted file mode 100644 index e82f1186..00000000 --- a/apps/api/internal/db/migrations/20251121165836_add_app_review_columns.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE events -ADD COLUMN application_review_started BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE applications - ADD COLUMN experience_rating INTEGER, - ADD COLUMN passion_rating INTEGER, - ADD COLUMN assigned_reviewer_id UUID REFERENCES auth.users(id) ON DELETE SET NULL; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE events -DROP COLUMN application_review_started; -ALTER TABLE applications - DROP COLUMN experience_rating, - DROP COLUMN passion_rating, - DROP COLUMN assigned_reviewer_id; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251208221807_add_application_waitlist_time_column.sql b/apps/api/internal/db/migrations/20251208221807_add_application_waitlist_time_column.sql deleted file mode 100644 index cc2713f0..00000000 --- a/apps/api/internal/db/migrations/20251208221807_add_application_waitlist_time_column.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE applications -ADD COLUMN waitlist_join_time TIMESTAMPTZ; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE applications -DROP COLUMN waitlist_join_time; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql b/apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql deleted file mode 100644 index da31c9c1..00000000 --- a/apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TYPE bat_run_status AS ENUM ('running','completed','failed'); - -CREATE TABLE bat_runs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, - accepted_applicants UUID[] DEFAULT '{}', - rejected_applicants UUID[] DEFAULT '{}', - status bat_run_status DEFAULT 'running', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - completed_at TIMESTAMPTZ -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS bat_runs; -DROP TYPE IF EXISTS bat_run_status; --- +goose StatementEnd - diff --git a/apps/api/internal/db/migrations/20251216200020_add_application_review_finished.sql b/apps/api/internal/db/migrations/20251216200020_add_application_review_finished.sql deleted file mode 100644 index 5a48ea40..00000000 --- a/apps/api/internal/db/migrations/20251216200020_add_application_review_finished.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE events -ADD COLUMN application_review_finished BOOLEAN NOT NULL DEFAULT FALSE; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE events -DROP COLUMN application_review_finished; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20251217065809_remove_application_review_finished_column.sql b/apps/api/internal/db/migrations/20251217065809_remove_application_review_finished_column.sql deleted file mode 100644 index 16f75232..00000000 --- a/apps/api/internal/db/migrations/20251217065809_remove_application_review_finished_column.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE events -DROP COLUMN application_review_finished --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE events -ADD COLUMN application_review_finished BOOLEAN NOT NULL DEFAULT FALSE; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20260116002956_checked_in_time.sql b/apps/api/internal/db/migrations/20260116002956_checked_in_time.sql deleted file mode 100644 index 53b84844..00000000 --- a/apps/api/internal/db/migrations/20260116002956_checked_in_time.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE event_roles -ADD COLUMN checked_in_at TIMESTAMPTZ, -ADD COLUMN rfid TEXT; - -CREATE UNIQUE INDEX idx_event_roles_rfid ON event_roles(rfid) -WHERE rfid IS NOT NULL; -- Only index non-nulls to optimize. --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE event_roles -DROP COLUMN checked_in_at, -DROP COLUMN rfid; - -DROP INDEX IF EXISTS idx_event_roles_rfid; --- +goose StatementEnd diff --git a/apps/api/internal/db/migrations/20260119015108_create_redeemables_tables.sql b/apps/api/internal/db/migrations/20260119015108_create_redeemables_tables.sql deleted file mode 100644 index c7fb3aef..00000000 --- a/apps/api/internal/db/migrations/20260119015108_create_redeemables_tables.sql +++ /dev/null @@ -1,43 +0,0 @@ --- +goose Up --- +goose StatementBegin -SELECT 'up SQL query'; -CREATE TABLE redeemables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - event_id UUID REFERENCES events(id) ON DELETE CASCADE NOT NULL, - name VARCHAR(255) NOT NULL, - amount INT NOT NULL CHECK (amount >= 0), - max_user_amount INT NOT NULL CHECK (max_user_amount >= 1), - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE TRIGGER set_updated_at_redeemables -BEFORE UPDATE ON redeemables -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - -CREATE TABLE user_redemptions ( - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, - redeemable_id UUID REFERENCES redeemables(id) ON DELETE CASCADE NOT NULL, - amount INT NOT NULL CHECK (amount >= 0), - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, redeemable_id) -); - -CREATE TRIGGER set_updated_at_user_redemptions -BEFORE UPDATE ON user_redemptions -FOR EACH ROW -EXECUTE FUNCTION update_modified_column(); - - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -SELECT 'down SQL query'; -DROP TABLE IF EXISTS user_redemptions; -DROP TRIGGER IF EXISTS set_updated_at_redeemables ON redeemables; -DROP TABLE IF EXISTS redeemables; -DROP TRIGGER IF EXISTS set_updated_at_user_redemptions ON user_redemptions; --- +goose StatementEnd diff --git a/apps/api/internal/db/queries/event_interest_submissions.sql b/apps/api/internal/db/queries/event_interest_submissions.sql deleted file mode 100644 index 475bf777..00000000 --- a/apps/api/internal/db/queries/event_interest_submissions.sql +++ /dev/null @@ -1,12 +0,0 @@ --- name: AddEmail :one --- Adds a new email to the mailing list for a specific user and event. --- The unique constraint on (event_id, user_id) will prevent duplicates. --- Returns the newly created email record. -INSERT INTO event_interest_submissions ( - event_id, - email, - source -) VALUES ( - $1, $2, $3 -) -RETURNING *; \ No newline at end of file diff --git a/apps/api/internal/db/queries/event_roles.sql b/apps/api/internal/db/queries/event_roles.sql deleted file mode 100644 index a57650a8..00000000 --- a/apps/api/internal/db/queries/event_roles.sql +++ /dev/null @@ -1,82 +0,0 @@ --- name: GetEventStaff :many -SELECT u.*, er.role AS event_role -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1 - AND er.role IN ('admin', 'staff'); - --- name: AssignRole :exec -INSERT INTO event_roles (event_id, user_id, role) -VALUES ($1, $2, $3) -ON CONFLICT DO NOTHING; - --- name: RemoveRole :exec -DELETE FROM event_roles -WHERE event_id = $1 - AND user_id = $2; - --- name: GetEventUsers :many -SELECT u.*, er.role AS event_role -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1; - --- name: UpdateRole :exec -UPDATE event_roles -SET role = $3 -WHERE event_id = $1 AND user_id = $2; - --- name: GetEventAttendeesWithDiscord :many -SELECT - a.account_id as discord_id, - u.id as user_id, - u.name, - u.email -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -JOIN auth.accounts a ON u.id = a.user_id -WHERE er.event_id = $1 - AND er.role = 'attendee' - AND a.provider_id = 'discord'; - --- name: GetEventRoleByDiscordIDAndEventId :one -SELECT er.event_id, er.role -FROM event_roles er -JOIN auth.accounts a ON er.user_id = a.user_id -WHERE a.provider_id = 'discord' - AND a.account_id = $1 - AND er.event_id = $2; --- name: UpdateEventRoleByIds :exec -UPDATE event_roles -SET - role = CASE WHEN @role_do_update::boolean THEN @role ELSE role END, - rfid = CASE WHEN @rfid_do_update::boolean THEN @rfid ELSE rfid END, - checked_in_at = CASE WHEN @checked_in_at_do_update::boolean THEN @checked_in_at ELSE checked_in_at END -WHERE user_id = @user_id - AND event_id = @event_id; - --- name: GetAttendeeCountByEventId :one -SELECT COUNT(*) FROM event_roles AS er -WHERE er.event_id = @event_id::uuid - AND er.role = 'attendee'; - --- name: GetAttendeeUserIdsByEventId :many -SELECT er.user_id FROM event_roles AS er -WHERE er.event_id = @event_id::uuid - AND er.role = 'attendee'; - --- name: GetUserByRFID :one -SELECT u.* -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1 - AND er.rfid = $2; - --- name: GetCheckedInStatusByIds :one -SELECT EXISTS ( - SELECT 1 - FROM event_roles - WHERE user_id = $1 - AND event_id = $2 - AND checked_in_at IS NOT NULL -)::bool; diff --git a/apps/api/internal/db/repository/application.go b/apps/api/internal/db/repository/application.go deleted file mode 100644 index 947d266c..00000000 --- a/apps/api/internal/db/repository/application.go +++ /dev/null @@ -1,217 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrCreateApplication = errors.New("unable to create application") - ErrSaveApplication = errors.New("unable to save application") - ErrSubmitApplication = errors.New("unable to submit application") - ErrInvalidApplicationData = errors.New("unable to parse application data") - ErrGetApplication = errors.New("unable to get application for user") - ErrApplicationNotFound = errors.New("can not find application for user") -) - -type ApplicationRepository struct { - db *db.DB -} - -func NewApplicationRepository(db *db.DB) *ApplicationRepository { - return &ApplicationRepository{ - db: db, - } -} - -func (r *ApplicationRepository) NewTx(tx pgx.Tx) *ApplicationRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &ApplicationRepository{ - db: txDB, - } -} - -func (r *ApplicationRepository) CreateApplication(ctx context.Context, params sqlc.CreateApplicationParams) (*sqlc.Application, error) { - application, err := r.db.Query.CreateApplication(ctx, params) - - if err != nil { - return nil, err - } - - return &application, nil -} - -func (r *ApplicationRepository) GetApplicationByUserAndEventID(ctx context.Context, params sqlc.GetApplicationByUserAndEventIDParams) (*sqlc.Application, error) { - application, err := r.db.Query.GetApplicationByUserAndEventID(ctx, params) - - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrApplicationNotFound - } - - return nil, err - } - - return &application, nil -} - -func (r *ApplicationRepository) UpdateApplicationStatusByEventId(ctx context.Context, status sqlc.ApplicationStatus, eventId uuid.UUID, userIds uuid.UUIDs) error { - return r.db.Query.UpdateApplicationStatusByEventID(ctx, sqlc.UpdateApplicationStatusByEventIDParams{ - EventID: eventId, - Status: status, - UserIds: userIds, - }) -} - -// List all candidates considered for admission for an eventId. -// This queries for all applications who are 'under_review' and have their rating fields filled out. -// It also LEFT JOINs in their team id (if they have one) for further grouping based on teams. -func (r *ApplicationRepository) ListAdmissionCandidatesByEvent(ctx context.Context, eventId uuid.UUID) ([]sqlc.ListAdmissionCandidatesByEventRow, error) { - return r.db.Query.ListAdmissionCandidatesByEvent(ctx, eventId) -} - -func (r *ApplicationRepository) SubmitApplication(ctx context.Context, data any, userId, eventId uuid.UUID) error { - jsonBytes, err := json.Marshal(data) - - if err != nil { - return err - } - - err = r.db.Query.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - StatusDoUpdate: true, - Status: sqlc.ApplicationStatusSubmitted, - ApplicationDoUpdate: true, - Application: jsonBytes, - SubmittedAtDoUpdate: true, - SubmittedAt: time.Now(), - SavedAtDoUpdate: true, - SavedAt: time.Now(), - UserID: userId, - EventID: eventId, - }) - - if err != nil { - return err - } - - return nil -} - -func (r *ApplicationRepository) SaveApplication(ctx context.Context, data any, userId, eventId uuid.UUID) error { - jsonBytes, err := json.Marshal(data) - - if err != nil { - return err - } - - err = r.db.Query.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - StatusDoUpdate: true, - Status: sqlc.ApplicationStatusStarted, - ApplicationDoUpdate: true, - Application: jsonBytes, - UserID: userId, - EventID: eventId, - SavedAtDoUpdate: true, - SavedAt: time.Now(), - }) - - if err != nil { - return err - } - - return nil -} - -func (r *ApplicationRepository) UpdateApplication(ctx context.Context, params sqlc.UpdateApplicationParams) error { - return r.db.Query.UpdateApplication(ctx, params) -} - -func (r *ApplicationRepository) ListAvailableApplicationForEvent(ctx context.Context, eventId uuid.UUID) ([]uuid.UUID, error) { - return r.db.Query.ListAvailableApplicationsForEvent(ctx, eventId) -} - -func (r *ApplicationRepository) AssignApplicationToReviewByEvent(ctx context.Context, reviewerId, eventId uuid.UUID, applicationIDs []uuid.UUID) error { - return r.db.Query.AssignApplicationsToReviewer(ctx, sqlc.AssignApplicationsToReviewerParams{ - ReviewerID: reviewerId, - EventID: eventId, - ApplicationIds: applicationIDs, - }) -} - -func (r *ApplicationRepository) ListApplicationByReviewerAndEvent(ctx context.Context, reviewerId, eventId uuid.UUID) ([]sqlc.ListApplicationByReviewerAndEventRow, error) { - return r.db.Query.ListApplicationByReviewerAndEvent(ctx, sqlc.ListApplicationByReviewerAndEventParams{ - AssignedReviewerID: &reviewerId, - EventID: eventId, - }) -} - -func (r *ApplicationRepository) ResetApplicationReviewsForEvent(ctx context.Context, eventId uuid.UUID) error { - return r.db.Query.ResetApplicationReviews(ctx, eventId) -} - -// Application statistics (Staff Dashboards) -func (r *ApplicationRepository) GetSubmittedApplicationGenders(ctx context.Context, eventId uuid.UUID) (sqlc.GetApplicationGenderSplitRow, error) { - return r.db.Query.GetApplicationGenderSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetSubmittedApplicationRaces(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetApplicationRaceSplitRow, error) { - return r.db.Query.GetApplicationRaceSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetSubmittedApplicationAges(ctx context.Context, eventId uuid.UUID) (sqlc.GetApplicationAgeSplitRow, error) { - return r.db.Query.GetApplicationAgeSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetSubmittedApplicationMajors(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetApplicationMajorSplitRow, error) { - return r.db.Query.GetApplicationMajorSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetSubmittedApplicationSchools(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetApplicationSchoolSplitRow, error) { - return r.db.Query.GetApplicationSchoolSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetApplicationStatuses(ctx context.Context, eventId uuid.UUID) (sqlc.GetApplicationStatusSplitRow, error) { - return r.db.Query.GetApplicationStatusSplit(ctx, eventId) -} - -func (r *ApplicationRepository) GetNonReviewedApplications(ctx context.Context, eventId uuid.UUID) ([]uuid.UUID, error) { - return r.db.Query.ListNonReviewedApplicationsByEvent(ctx, eventId) -} - -func (r *ApplicationRepository) GetSubmissionTimes(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetSubmissionTimesRow, error) { - return r.db.Query.GetSubmissionTimes(ctx, eventId) -} - -func (r *ApplicationRepository) JoinWaitlist(ctx context.Context, userId, eventId uuid.UUID) error { - return r.db.Query.JoinWaitlist(ctx, sqlc.JoinWaitlistParams{ - UserID: userId, - EventID: eventId, - }) -} - -func (r *ApplicationRepository) TransitionAcceptedApplicationsToWaitlistByEventID(ctx context.Context, eventId uuid.UUID) error { - return r.db.Query.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) -} - -func (r *ApplicationRepository) TransitionWaitlistedApplicationsToAcceptedByEventID(ctx context.Context, eventId uuid.UUID, acceptanceCount uint32) ([]uuid.UUID, error) { - return r.db.Query.TransitionWaitlistedApplicationsToAcceptedByEventID(ctx, sqlc.TransitionWaitlistedApplicationsToAcceptedByEventIDParams{ - EventID: eventId, - Acceptancecount: int32(acceptanceCount), - }) -} - -func (r *ApplicationRepository) GetAttendeeCountByEventId(ctx context.Context, eventId uuid.UUID) (uint32, error) { - amount, err := r.db.Query.GetAttendeeCountByEventId(ctx, eventId) - return uint32(amount), err -} diff --git a/apps/api/internal/db/repository/bat_runs.go b/apps/api/internal/db/repository/bat_runs.go deleted file mode 100644 index 398cc804..00000000 --- a/apps/api/internal/db/repository/bat_runs.go +++ /dev/null @@ -1,74 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrDuplicateRun = errors.New("Run already exists in the database") - ErrRunNotFound = errors.New("Run not found") - ErrNoRunsDeleted = errors.New("No Runs deleted") - ErrMultipleRunsDeleted = errors.New("Multiple Runs affected by delete query expecting to delete one") -) - -type BatRunsRepository struct { - db *db.DB -} - -func NewBatRunsRepository(db *db.DB) *BatRunsRepository { - return &BatRunsRepository{ - db: db, - } -} - -func (r *BatRunsRepository) AddRun(ctx context.Context, eventId uuid.UUID) (*sqlc.BatRun, error) { - run, err := r.db.Query.AddRun(ctx, eventId) - if err != nil { - if db.IsUniqueViolation(err) { - return nil, ErrDuplicateRun - } - return nil, err - } - return &run, nil -} - -func (r *BatRunsRepository) GetRunById(ctx context.Context, id uuid.UUID) (sqlc.BatRun, error) { - return r.db.Query.GetRunById(ctx, id) -} - -func (r *BatRunsRepository) GetRunsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetRunsByEventIdRow, error) { - runs, err := r.db.Query.GetRunsByEventId(ctx, eventId) - return &runs, err -} - -func (r *BatRunsRepository) UpdateRunById(ctx context.Context, params sqlc.UpdateRunByIdParams) error { - err := r.db.Query.UpdateRunById(ctx, params) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrEventNotFound - } - } - return err -} - -func (r *BatRunsRepository) DeleteRunById(ctx context.Context, id uuid.UUID) error { - affectedRows, err := r.db.Query.DeleteRunById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrRunNotFound - } - } - if affectedRows == 0 { - return ErrNoRunsDeleted - } else if affectedRows > 1 { - return ErrMultipleRunsDeleted - } - - return err -} diff --git a/apps/api/internal/db/repository/event_interest.go b/apps/api/internal/db/repository/event_interest.go deleted file mode 100644 index 886ae9fc..00000000 --- a/apps/api/internal/db/repository/event_interest.go +++ /dev/null @@ -1,36 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrDuplicateEmails = errors.New("email already exists in the database") -) - -type EventInterestRepository struct { - db *db.DB -} - -func NewEventInterestRepository(db *db.DB) *EventInterestRepository { - return &EventInterestRepository{ - db: db, - } -} - -func (r *EventInterestRepository) AddEmail(ctx context.Context, params sqlc.AddEmailParams) (*sqlc.EventInterestSubmission, error) { - interestSubmission, err := r.db.Query.AddEmail(ctx, params) - if err != nil { - if db.IsUniqueViolation(err) { - return nil, ErrDuplicateEmails - } - - return nil, err - } - - return &interestSubmission, nil -} diff --git a/apps/api/internal/db/repository/events.go b/apps/api/internal/db/repository/events.go deleted file mode 100644 index 4599383a..00000000 --- a/apps/api/internal/db/repository/events.go +++ /dev/null @@ -1,227 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrEventNotFound = errors.New("event not found") - ErrEventRoleNotFound = errors.New("event role not found") - ErrDuplicateEvent = errors.New("event already exists in database") - ErrNoEventsDeleted = errors.New("no events deleted") - ErrMultipleEventsDeleted = errors.New("multiple events affected by delete query while only expecting one to delete one") - ErrUserEventNotFound = errors.New("the user and event id combination was not found") - ErrUnknown = errors.New("an unkown error was caught") -) - -type EventRepository struct { - db *db.DB -} - -func NewEventRespository(db *db.DB) *EventRepository { - return &EventRepository{ - db: db, - } -} - -func (r *EventRepository) NewTx(tx pgx.Tx) *EventRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &EventRepository{ - db: txDB, - } -} - -func (r *EventRepository) CreateEvent(ctx context.Context, params sqlc.CreateEventParams) (*sqlc.Event, error) { - event, err := r.db.Query.CreateEvent(ctx, params) - if db.IsUniqueViolation(err) { - return nil, ErrDuplicateEvent - } - return &event, err -} - -func (r *EventRepository) GetEventByID(ctx context.Context, id uuid.UUID) (*sqlc.Event, error) { - event, err := r.db.Query.GetEventByID(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrEventNotFound - } - return nil, err - } - return &event, err -} - -func (r *EventRepository) UpdateEventById(ctx context.Context, params sqlc.UpdateEventByIdParams) error { - err := r.db.Query.UpdateEventById(ctx, params) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrEventNotFound - } - } - return err -} - -func (r *EventRepository) DeleteEventById(ctx context.Context, id uuid.UUID) error { - affectedRows, err := r.db.Query.DeleteEventById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrEventNotFound - } - } - if affectedRows == 0 { - return ErrNoEventsDeleted - } else if affectedRows > 1 { - return ErrMultipleEventsDeleted - } - - return err -} - -func (r *EventRepository) GetAllEvents(ctx context.Context, userId uuid.UUID) (*[]sqlc.GetAllEventsRow, error) { - events, err := r.db.Query.GetAllEvents(ctx, userId) - return &events, err -} - -func (r *EventRepository) GetPublishedEvents(ctx context.Context, userId uuid.UUID) (*[]sqlc.GetPublishedEventsRow, error) { - events, err := r.db.Query.GetPublishedEvents(ctx, userId) - return &events, err -} - -// find a struct for valid roles to enforce type safety -func (r *EventRepository) GetEventsWithRoles(ctx context.Context, userId *uuid.UUID, scope sqlc.GetEventScopeType) (*[]sqlc.GetEventsWithUserInfoRow, error) { - events, err := r.db.Query.GetEventsWithUserInfo(ctx, sqlc.GetEventsWithUserInfoParams{ - UserID: userId, - Scope: scope, - }) - return &events, err -} - -func (r *EventRepository) GetEventRoleByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (*sqlc.EventRole, error) { - params := sqlc.GetEventRoleByIdsParams{ - UserID: userId, - EventID: eventId, - } - - eventRole, err := r.db.Query.GetEventRoleByIds(ctx, params) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrEventRoleNotFound - } - } - - return &eventRole, err -} - -func (r *EventRepository) GetUserByRFID(ctx context.Context, eventId uuid.UUID, rfid string) (*sqlc.AuthUser, error) { - params := sqlc.GetUserByRFIDParams{ - EventID: eventId, - Rfid: &rfid, - } - user, err := r.db.Query.GetUserByRFID(ctx, params) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrEventRoleNotFound - } - return nil, ErrUnknown - } - return &user, nil -} - -func (r *EventRepository) GetEventStaff(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetEventStaffRow, error) { - users, err := r.db.Query.GetEventStaff(ctx, eventId) - return &users, err -} - -func (r *EventRepository) GetEventUsers(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetEventUsersRow, error) { - users, err := r.db.Query.GetEventUsers(ctx, eventId) - return &users, err -} - -func (r *EventRepository) AssignRole(ctx context.Context, params sqlc.AssignRoleParams) error { - return r.db.Query.AssignRole(ctx, params) -} - -func (r *EventRepository) RevokeRole(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { - params := sqlc.RemoveRoleParams{ - UserID: userId, - EventID: eventId, - } - - return r.db.Query.RemoveRole(ctx, params) -} - -// **Deprecated**. Use `UpdateEventRoleByIds` instead. -// Only kept for backwards compatibility. TODO: Refactor all current implementations to use new function. -func (r *EventRepository) UpdateRole(ctx context.Context, userId uuid.UUID, eventId uuid.UUID, role sqlc.EventRoleType) error { - params := sqlc.UpdateRoleParams{ - UserID: userId, - EventID: eventId, - Role: role, - } - return r.db.Query.UpdateRole(ctx, params) -} - -func (r *EventRepository) UpdateEventRoleByIds(ctx context.Context, params sqlc.UpdateEventRoleByIdsParams) error { - return r.db.Query.UpdateEventRoleByIds(ctx, params) -} - -func (r *EventRepository) GetApplicationStatuses(ctx context.Context, eventId uuid.UUID) (sqlc.GetApplicationStatusSplitRow, error) { - return r.db.Query.GetApplicationStatusSplit(ctx, eventId) -} - -func (r *EventRepository) GetSubmissionTimes(ctx context.Context, eventId uuid.UUID) ([]sqlc.GetSubmissionTimesRow, error) { - return r.db.Query.GetSubmissionTimes(ctx, eventId) -} - -func (r *EventRepository) GetEventAttendeesWithDiscord(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetEventAttendeesWithDiscordRow, error) { - attendees, err := r.db.Query.GetEventAttendeesWithDiscord(ctx, eventId) - if err != nil { - return nil, err - } - return &attendees, nil -} - -func (r *EventRepository) GetEventRoleByDiscordIDAndEventId(ctx context.Context, discordID string, eventID uuid.UUID) (*sqlc.GetEventRoleByDiscordIDAndEventIdRow, error) { - params := sqlc.GetEventRoleByDiscordIDAndEventIdParams{ - AccountID: discordID, - EventID: eventID, - } - - eventRole, err := r.db.Query.GetEventRoleByDiscordIDAndEventId(ctx, params) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrEventRoleNotFound - } - return nil, err - } - - return &eventRole, nil -} - -func (r *EventRepository) GetCheckedInStatusByUserIdAndEventId(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) { - params := sqlc.GetCheckedInStatusByIdsParams{ - UserID: userId, - EventID: eventId, - } - - result, err := r.db.Query.GetCheckedInStatusByIds(ctx, params) - - if err != nil { - return false, err - } - - return result, nil -} - -func (r *EventRepository) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { - return r.db.Query.GetAttendeeUserIdsByEventId(ctx, eventID) -} diff --git a/apps/api/internal/db/repository/redeemables.go b/apps/api/internal/db/repository/redeemables.go deleted file mode 100644 index 4270d0cf..00000000 --- a/apps/api/internal/db/repository/redeemables.go +++ /dev/null @@ -1,95 +0,0 @@ -package repository - -import ( - "context" - - "github.com/google/uuid" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -type RedeemablesRepository struct { - db *db.DB -} - -func NewRedeemablesRepository(db *db.DB) *RedeemablesRepository { - return &RedeemablesRepository{ - db: db, - } -} - -func (r *RedeemablesRepository) GetRedeemablesByEventID(ctx context.Context, eventID uuid.UUID) (*[]sqlc.GetRedeemablesByEventIDRow, error) { - redeemables, err := r.db.Query.GetRedeemablesByEventID(ctx, eventID) - if err != nil { - return nil, err - } - return &redeemables, nil -} - -func (r *RedeemablesRepository) CreateRedeemable(ctx context.Context, eventID uuid.UUID, name string, amount int, maxUserAmount int) (*sqlc.Redeemable, error) { - params := sqlc.CreateRedeemableParams{ - EventID: eventID, - Name: name, - Amount: int32(amount), - MaxUserAmount: int32(maxUserAmount), - } - redeemable, err := r.db.Query.CreateRedeemable(ctx, params) - if err != nil { - return nil, err - } - return &redeemable, nil -} -func (r *RedeemablesRepository) DeleteRedeemable(ctx context.Context, redeemableID uuid.UUID) error { - err := r.db.Query.DeleteRedeemable(ctx, redeemableID) - if err != nil { - return err - } - return nil -} -func (r *RedeemablesRepository) UpdateRedeemable(ctx context.Context, redeemableID uuid.UUID, name *string, amount *int, maxUserAmount *int) (*sqlc.Redeemable, error) { - var amount32 *int32 - if amount != nil { - v := int32(*amount) - amount32 = &v - } - var maxUserAmount32 *int32 - if maxUserAmount != nil { - v := int32(*maxUserAmount) - maxUserAmount32 = &v - } - params := sqlc.UpdateRedeemableParams{ - ID: redeemableID, - Name: name, - Amount: amount32, - MaxUserAmount: maxUserAmount32, - } - redeemable, err := r.db.Query.UpdateRedeemable(ctx, params) - if err != nil { - return nil, err - } - return &redeemable, nil -} - -func (r *RedeemablesRepository) RedeemRedeemable(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID) (*sqlc.UserRedemption, error) { - params := sqlc.RedeemRedeemableParams{ - RedeemableID: redeemableID, - UserID: userID, - } - redemption, err := r.db.Query.RedeemRedeemable(ctx, params) - if err != nil { - return nil, err - } - return &redemption, nil -} - -func (r *RedeemablesRepository) UpdateRedemption(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID, amount int) error { - err := r.db.Query.UpdateRedemption(ctx, sqlc.UpdateRedemptionParams{ - RedeemableID: redeemableID, - UserID: userID, - Amount: int32(amount), - }) - if err != nil { - return err - } - return nil -} diff --git a/apps/api/internal/db/repository/team_join_requests.go b/apps/api/internal/db/repository/team_join_requests.go deleted file mode 100644 index 59eaf8c9..00000000 --- a/apps/api/internal/db/repository/team_join_requests.go +++ /dev/null @@ -1,104 +0,0 @@ -package repository - -import ( - "context" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/logger" -) - -type TeamJoinRequestRepository struct { - db *db.DB -} - -func NewTeamJoinRequestRepository(db *db.DB) *TeamJoinRequestRepository { - return &TeamJoinRequestRepository{ - db: db, - } -} - -func (r *TeamJoinRequestRepository) NewTx(tx pgx.Tx) *TeamJoinRequestRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &TeamJoinRequestRepository{ - db: txDB, - } -} - -func (r *TeamJoinRequestRepository) Create(ctx context.Context, teamId, userId uuid.UUID, message *string) (*sqlc.TeamJoinRequest, error) { - - l := logger.New() - l.Debug().Msg("Before creating...") - request, err := r.db.Query.CreateTeamJoinRequest(ctx, sqlc.CreateTeamJoinRequestParams{ - TeamID: teamId, - UserID: userId, - RequestMessage: message, - }) - if err != nil { - return nil, err - } - - return &request, nil -} - -func (r *TeamJoinRequestRepository) GetById(ctx context.Context, requestId uuid.UUID) (*sqlc.TeamJoinRequest, error) { - request, err := r.db.Query.GetTeamJoinRequestByID(ctx, requestId) - if err != nil { - return nil, err - } - - return &request, nil -} - -func (r *TeamJoinRequestRepository) ListJoinRequestsByUser(ctx context.Context, userId uuid.UUID) ([]sqlc.TeamJoinRequest, error) { - return r.db.Query.ListTeamJoinRequestsByUserID(ctx, userId) -} - -func (r *TeamJoinRequestRepository) ListJoinRequestsByTeam(ctx context.Context, teamId uuid.UUID, status sqlc.JoinRequestStatus) ([]sqlc.TeamJoinRequest, error) { - return r.db.Query.ListTeamJoinRequestsByTeamIDAndStatus(ctx, sqlc.ListTeamJoinRequestsByTeamIDAndStatusParams{ - TeamID: teamId, - Status: status, - }) -} - -func (r *TeamJoinRequestRepository) ListJoinRequestsByTeamWithUser(ctx context.Context, teamId uuid.UUID, status sqlc.JoinRequestStatus) ([]sqlc.ListJoinRequestsByTeamAndStatusWithUserRow, error) { - return r.db.Query.ListJoinRequestsByTeamAndStatusWithUser(ctx, sqlc.ListJoinRequestsByTeamAndStatusWithUserParams{ - TeamID: teamId, - Status: status, - }) -} - -func (r *TeamJoinRequestRepository) ListJoinRequestsByUserAndEvent(ctx context.Context, userId, eventId uuid.UUID, status sqlc.JoinRequestStatus) ([]sqlc.TeamJoinRequest, error) { - return r.db.Query.ListTeamJoinRequestsByUserAndEventAndStatus(ctx, sqlc.ListTeamJoinRequestsByUserAndEventAndStatusParams{ - UserID: userId, - EventID: &eventId, - Status: status, - }) -} - -func (r *TeamJoinRequestRepository) DeleteByUserAndEventAndStatus(ctx context.Context, userId, eventId uuid.UUID, status sqlc.JoinRequestStatus) error { - return r.db.Query.DeleteJoinRequestsByUserAndEventAndStatus(ctx, sqlc.DeleteJoinRequestsByUserAndEventAndStatusParams{ - UserID: userId, - EventID: &eventId, - Status: status, - }) -} - -func (r *TeamJoinRequestRepository) UpdateStatus(ctx context.Context, requestId uuid.UUID, status sqlc.JoinRequestStatus) (*sqlc.TeamJoinRequest, error) { - request, err := r.db.Query.UpdateTeamJoinRequest(ctx, sqlc.UpdateTeamJoinRequestParams{ - ID: requestId, - StatusDoUpdate: true, - Status: status, - }) - if err != nil { - return nil, err - } - - return &request, nil -} diff --git a/apps/api/internal/db/repository/team_members.go b/apps/api/internal/db/repository/team_members.go deleted file mode 100644 index c9ad75fa..00000000 --- a/apps/api/internal/db/repository/team_members.go +++ /dev/null @@ -1,68 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/ptr" -) - -var ( - ErrTeamMemberNotFound = errors.New("team member not found") -) - -type TeamMemberRepository struct { - db *db.DB -} - -func NewTeamMemberRespository(db *db.DB) *TeamMemberRepository { - return &TeamMemberRepository{ - db: db, - } -} - -func (r *TeamMemberRepository) NewTx(tx pgx.Tx) *TeamMemberRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &TeamMemberRepository{ - db: txDB, - } -} - -func (r *TeamMemberRepository) GetTeamMembers(ctx context.Context, teamId uuid.UUID) ([]sqlc.GetTeamMembersRow, error) { - return r.db.Query.GetTeamMembers(ctx, teamId) -} - -func (r *TeamMemberRepository) GetTeamMemberByUserAndEvent(ctx context.Context, userId, eventId uuid.UUID) (*sqlc.TeamMember, error) { - member, err := r.db.Query.GetTeamMemberByUserAndEvent(ctx, sqlc.GetTeamMemberByUserAndEventParams{ - UserID: userId, - EventID: ptr.UUIDToPtr(eventId), - }) - if err != nil && db.IsNotFound(err) { - return nil, ErrTeamMemberNotFound - } - return &member, err -} - -func (r *TeamMemberRepository) Create(ctx context.Context, teamId, userId uuid.UUID) (*sqlc.TeamMember, error) { - member, err := r.db.Query.CreateTeamMember(ctx, sqlc.CreateTeamMemberParams{ - TeamID: teamId, - UserID: userId, - }) - - return &member, err -} - -func (r *TeamMemberRepository) Delete(ctx context.Context, teamId, userId uuid.UUID) error { - return r.db.Query.RemoveTeamMember(ctx, sqlc.RemoveTeamMemberParams{ - UserID: userId, - TeamID: teamId, - }) -} diff --git a/apps/api/internal/db/repository/teams.go b/apps/api/internal/db/repository/teams.go deleted file mode 100644 index 0ff4ecbc..00000000 --- a/apps/api/internal/db/repository/teams.go +++ /dev/null @@ -1,102 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/ptr" -) - -var ( - ErrTeamNotFound = errors.New("team was not found") -) - -type TeamRepository struct { - db *db.DB -} - -func NewTeamRespository(db *db.DB) *TeamRepository { - return &TeamRepository{ - db: db, - } -} - -func (r *TeamRepository) NewTx(tx pgx.Tx) *TeamRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &TeamRepository{ - db: txDB, - } -} - -func (r *TeamRepository) Create(ctx context.Context, name string, owner_id, event_id uuid.UUID) (*sqlc.Team, error) { - team, err := r.db.Query.CreateTeam(ctx, sqlc.CreateTeamParams{ - Name: name, - OwnerID: ptr.UUIDToPtr(owner_id), - EventID: ptr.UUIDToPtr(event_id), - }) - if err != nil { - return nil, err - } - - return &team, err -} - -func (r *TeamRepository) GetByID(ctx context.Context, teamId uuid.UUID) (*sqlc.Team, error) { - team, err := r.db.Query.GetTeamById(ctx, teamId) - if err != nil && db.IsNotFound(err) { - return nil, ErrTeamNotFound - } - - return &team, err -} - -func (r *TeamRepository) GetTeamByMemberAndEvent(ctx context.Context, userId, eventId uuid.UUID) (*sqlc.GetUserEventTeamRow, error) { - team, err := r.db.Query.GetUserEventTeam(ctx, sqlc.GetUserEventTeamParams{ - UserID: userId, - EventID: ptr.UUIDToPtr(eventId), - }) - - if err != nil && db.IsNotFound(err) { - return nil, ErrTeamNotFound - } - - return &team, err -} - -func (r *TeamRepository) GetTeamsWithMembersByEvent(ctx context.Context, eventId uuid.UUID, limit, offset int32) ([]sqlc.ListTeamsWithMembersByEventRow, error) { - return r.db.Query.ListTeamsWithMembersByEvent(ctx, sqlc.ListTeamsWithMembersByEventParams{ - EventID: ptr.UUIDToPtr(eventId), - Limit: limit, - Offset: offset, - }) -} - -func (r *TeamRepository) Delete(ctx context.Context, teamId uuid.UUID) error { - return r.db.Query.DeleteTeam(ctx, teamId) -} - -func (r *TeamRepository) Update(ctx context.Context, teamId uuid.UUID, name *string, ownerId *uuid.UUID) (*sqlc.Team, error) { - params := sqlc.UpdateTeamByIdParams{ - ID: teamId, - OwnerIDDoUpdate: ownerId != nil && *ownerId != uuid.Nil, - NameDoUpdate: name != nil && *name != "", - } - - if ownerId != nil { - params.OwnerID = ownerId - } - if name != nil { - params.Name = *name - } - - team, err := r.db.Query.UpdateTeamById(ctx, params) - return &team, err -} diff --git a/apps/api/internal/db/repository/users.go b/apps/api/internal/db/repository/users.go deleted file mode 100644 index 75ba3aac..00000000 --- a/apps/api/internal/db/repository/users.go +++ /dev/null @@ -1,97 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrUserNotFound = errors.New("user not found") -) - -type UserRepository struct { - db *db.DB -} - -func NewUserRepository(db *db.DB) *UserRepository { - return &UserRepository{ - db: db, - } -} - -// Call this to create a copy with transactional queries -func (r *UserRepository) NewTx(tx pgx.Tx) *UserRepository { - txDB := &db.DB{ - Pool: r.db.Pool, - Query: sqlc.New(tx), - } - - return &UserRepository{db: txDB} -} - -func (r *UserRepository) Create(ctx context.Context, params sqlc.CreateUserParams) (*sqlc.AuthUser, error) { - user, err := r.db.Query.CreateUser(ctx, params) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*sqlc.AuthUser, error) { - user, err := r.db.Query.GetUserByID(ctx, id) - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrUserNotFound - } else if err != nil { - return nil, err - } - - return &user, nil -} - -func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*sqlc.AuthUser, error) { - user, err := r.db.Query.GetUserByEmail(ctx, &email) - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrUserNotFound - } else if err != nil { - return nil, err - } - - return &user, nil -} - -func (r *UserRepository) GetUserEmailInfoById(ctx context.Context, id uuid.UUID) (*sqlc.GetUserEmailInfoByIdRow, error) { - row, err := r.db.Query.GetUserEmailInfoById(ctx, id) - if errors.Is(err, pgx.ErrNoRows) { - return nil, ErrUserNotFound - } else if err != nil { - return nil, err - } - - return &row, nil -} - -func (r *UserRepository) UpdateUser(ctx context.Context, params sqlc.UpdateUserParams) error { - err := r.db.Query.UpdateUser(ctx, params) - if err != nil { - if err == pgx.ErrNoRows { - return ErrUserNotFound - } - } - return err -} - -func (r *UserRepository) GetAllUsers(ctx context.Context, search *string, limit, offset int32) ([]sqlc.AuthUser, error) { - params := sqlc.GetUsersParams{ - Search: search, - Limit: limit, - Offset: offset, - } - - return r.db.Query.GetUsers(ctx, params) -} diff --git a/apps/api/internal/db/sqlc/bat_runs.sql.go b/apps/api/internal/db/sqlc/bat_runs.sql.go deleted file mode 100644 index d43d44cb..00000000 --- a/apps/api/internal/db/sqlc/bat_runs.sql.go +++ /dev/null @@ -1,158 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: bat_runs.sql - -package sqlc - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const addRun = `-- name: AddRun :one -INSERT INTO bat_runs ( - event_id -) VALUES ( - $1 -) RETURNING id, event_id, accepted_applicants, rejected_applicants, status, created_at, completed_at -` - -func (q *Queries) AddRun(ctx context.Context, eventID uuid.UUID) (BatRun, error) { - row := q.db.QueryRow(ctx, addRun, eventID) - var i BatRun - err := row.Scan( - &i.ID, - &i.EventID, - &i.AcceptedApplicants, - &i.RejectedApplicants, - &i.Status, - &i.CreatedAt, - &i.CompletedAt, - ) - return i, err -} - -const deleteRunById = `-- name: DeleteRunById :execrows -DELETE FROM bat_runs -WHERE id = $1 -` - -func (q *Queries) DeleteRunById(ctx context.Context, id uuid.UUID) (int64, error) { - result, err := q.db.Exec(ctx, deleteRunById, id) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - -const getRunById = `-- name: GetRunById :one -SELECT id, event_id, accepted_applicants, rejected_applicants, status, created_at, completed_at -FROM bat_runs -WHERE id = $1 -` - -func (q *Queries) GetRunById(ctx context.Context, id uuid.UUID) (BatRun, error) { - row := q.db.QueryRow(ctx, getRunById, id) - var i BatRun - err := row.Scan( - &i.ID, - &i.EventID, - &i.AcceptedApplicants, - &i.RejectedApplicants, - &i.Status, - &i.CreatedAt, - &i.CompletedAt, - ) - return i, err -} - -const getRunsByEventId = `-- name: GetRunsByEventId :many -SELECT - id, - accepted_applicants, - rejected_applicants, - status, - created_at, - completed_at -FROM bat_runs -WHERE event_id = $1 -ORDER BY created_at DESC -` - -type GetRunsByEventIdRow struct { - ID uuid.UUID `json:"id"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` - Status NullBatRunStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - CompletedAt *time.Time `json:"completed_at"` -} - -func (q *Queries) GetRunsByEventId(ctx context.Context, eventID uuid.UUID) ([]GetRunsByEventIdRow, error) { - rows, err := q.db.Query(ctx, getRunsByEventId, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetRunsByEventIdRow{} - for rows.Next() { - var i GetRunsByEventIdRow - if err := rows.Scan( - &i.ID, - &i.AcceptedApplicants, - &i.RejectedApplicants, - &i.Status, - &i.CreatedAt, - &i.CompletedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateRunById = `-- name: UpdateRunById :exec -UPDATE bat_runs -SET - accepted_applicants = CASE WHEN $1::boolean THEN $2 ELSE accepted_applicants END, - rejected_applicants = CASE WHEN $3::boolean THEN $4 ELSE rejected_applicants END, - status = CASE WHEN $5::boolean THEN $6 ELSE status END, - created_at = CASE WHEN $7::boolean THEN $8 ELSE created_at END -WHERE - id = $9::uuid -RETURNING id, event_id, accepted_applicants, rejected_applicants, status, created_at, completed_at -` - -type UpdateRunByIdParams struct { - AcceptedApplicantsDoUpdate bool `json:"accepted_applicants_do_update"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicantsDoUpdate bool `json:"rejected_applicants_do_update"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` - StatusDoUpdate bool `json:"status_do_update"` - Status NullBatRunStatus `json:"status"` - CreatedAtDoUpdate bool `json:"created_at_do_update"` - CreatedAt time.Time `json:"created_at"` - ID uuid.UUID `json:"id"` -} - -func (q *Queries) UpdateRunById(ctx context.Context, arg UpdateRunByIdParams) error { - _, err := q.db.Exec(ctx, updateRunById, - arg.AcceptedApplicantsDoUpdate, - arg.AcceptedApplicants, - arg.RejectedApplicantsDoUpdate, - arg.RejectedApplicants, - arg.StatusDoUpdate, - arg.Status, - arg.CreatedAtDoUpdate, - arg.CreatedAt, - arg.ID, - ) - return err -} diff --git a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go deleted file mode 100644 index 27239b85..00000000 --- a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: event_interest_submissions.sql - -package sqlc - -import ( - "context" - - "github.com/google/uuid" -) - -const addEmail = `-- name: AddEmail :one -INSERT INTO event_interest_submissions ( - event_id, - email, - source -) VALUES ( - $1, $2, $3 -) -RETURNING id, event_id, email, created_at, source -` - -type AddEmailParams struct { - EventID uuid.UUID `json:"event_id"` - Email string `json:"email"` - Source *string `json:"source"` -} - -// Adds a new email to the mailing list for a specific user and event. -// The unique constraint on (event_id, user_id) will prevent duplicates. -// Returns the newly created email record. -func (q *Queries) AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) { - row := q.db.QueryRow(ctx, addEmail, arg.EventID, arg.Email, arg.Source) - var i EventInterestSubmission - err := row.Scan( - &i.ID, - &i.EventID, - &i.Email, - &i.CreatedAt, - &i.Source, - ) - return i, err -} diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go deleted file mode 100644 index 5cdea072..00000000 --- a/apps/api/internal/db/sqlc/event_roles.sql.go +++ /dev/null @@ -1,374 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: event_roles.sql - -package sqlc - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const assignRole = `-- name: AssignRole :exec -INSERT INTO event_roles (event_id, user_id, role) -VALUES ($1, $2, $3) -ON CONFLICT DO NOTHING -` - -type AssignRoleParams struct { - EventID uuid.UUID `json:"event_id"` - UserID uuid.UUID `json:"user_id"` - Role EventRoleType `json:"role"` -} - -func (q *Queries) AssignRole(ctx context.Context, arg AssignRoleParams) error { - _, err := q.db.Exec(ctx, assignRole, arg.EventID, arg.UserID, arg.Role) - return err -} - -const getAttendeeCountByEventId = `-- name: GetAttendeeCountByEventId :one -SELECT COUNT(*) FROM event_roles AS er -WHERE er.event_id = $1::uuid - AND er.role = 'attendee' -` - -func (q *Queries) GetAttendeeCountByEventId(ctx context.Context, eventID uuid.UUID) (int64, error) { - row := q.db.QueryRow(ctx, getAttendeeCountByEventId, eventID) - var count int64 - err := row.Scan(&count) - return count, err -} - -const getAttendeeUserIdsByEventId = `-- name: GetAttendeeUserIdsByEventId :many -SELECT er.user_id FROM event_roles AS er -WHERE er.event_id = $1::uuid - AND er.role = 'attendee' -` - -func (q *Queries) GetAttendeeUserIdsByEventId(ctx context.Context, eventID uuid.UUID) ([]uuid.UUID, error) { - rows, err := q.db.Query(ctx, getAttendeeUserIdsByEventId, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []uuid.UUID{} - for rows.Next() { - var user_id uuid.UUID - if err := rows.Scan(&user_id); err != nil { - return nil, err - } - items = append(items, user_id) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getCheckedInStatusByIds = `-- name: GetCheckedInStatusByIds :one -SELECT EXISTS ( - SELECT 1 - FROM event_roles - WHERE user_id = $1 - AND event_id = $2 - AND checked_in_at IS NOT NULL -)::bool -` - -type GetCheckedInStatusByIdsParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) GetCheckedInStatusByIds(ctx context.Context, arg GetCheckedInStatusByIdsParams) (bool, error) { - row := q.db.QueryRow(ctx, getCheckedInStatusByIds, arg.UserID, arg.EventID) - var column_1 bool - err := row.Scan(&column_1) - return column_1, err -} - -const getEventAttendeesWithDiscord = `-- name: GetEventAttendeesWithDiscord :many -SELECT - a.account_id as discord_id, - u.id as user_id, - u.name, - u.email -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -JOIN auth.accounts a ON u.id = a.user_id -WHERE er.event_id = $1 - AND er.role = 'attendee' - AND a.provider_id = 'discord' -` - -type GetEventAttendeesWithDiscordRow struct { - DiscordID string `json:"discord_id"` - UserID uuid.UUID `json:"user_id"` - Name string `json:"name"` - Email *string `json:"email"` -} - -func (q *Queries) GetEventAttendeesWithDiscord(ctx context.Context, eventID uuid.UUID) ([]GetEventAttendeesWithDiscordRow, error) { - rows, err := q.db.Query(ctx, getEventAttendeesWithDiscord, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetEventAttendeesWithDiscordRow{} - for rows.Next() { - var i GetEventAttendeesWithDiscordRow - if err := rows.Scan( - &i.DiscordID, - &i.UserID, - &i.Name, - &i.Email, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getEventRoleByDiscordIDAndEventId = `-- name: GetEventRoleByDiscordIDAndEventId :one -SELECT er.event_id, er.role -FROM event_roles er -JOIN auth.accounts a ON er.user_id = a.user_id -WHERE a.provider_id = 'discord' - AND a.account_id = $1 - AND er.event_id = $2 -` - -type GetEventRoleByDiscordIDAndEventIdParams struct { - AccountID string `json:"account_id"` - EventID uuid.UUID `json:"event_id"` -} - -type GetEventRoleByDiscordIDAndEventIdRow struct { - EventID uuid.UUID `json:"event_id"` - Role EventRoleType `json:"role"` -} - -func (q *Queries) GetEventRoleByDiscordIDAndEventId(ctx context.Context, arg GetEventRoleByDiscordIDAndEventIdParams) (GetEventRoleByDiscordIDAndEventIdRow, error) { - row := q.db.QueryRow(ctx, getEventRoleByDiscordIDAndEventId, arg.AccountID, arg.EventID) - var i GetEventRoleByDiscordIDAndEventIdRow - err := row.Scan(&i.EventID, &i.Role) - return i, err -} - -const getEventStaff = `-- name: GetEventStaff :many -SELECT u.id, u.name, u.email, u.email_verified, u.onboarded, u.image, u.created_at, u.updated_at, u.role, u.preferred_email, u.email_consent, er.role AS event_role -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1 - AND er.role IN ('admin', 'staff') -` - -type GetEventStaffRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Email *string `json:"email"` - EmailVerified bool `json:"email_verified"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Role AuthUserRole `json:"role"` - PreferredEmail *string `json:"preferred_email"` - EmailConsent bool `json:"email_consent"` - EventRole EventRoleType `json:"event_role"` -} - -func (q *Queries) GetEventStaff(ctx context.Context, eventID uuid.UUID) ([]GetEventStaffRow, error) { - rows, err := q.db.Query(ctx, getEventStaff, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetEventStaffRow{} - for rows.Next() { - var i GetEventStaffRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Email, - &i.EmailVerified, - &i.Onboarded, - &i.Image, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.PreferredEmail, - &i.EmailConsent, - &i.EventRole, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getEventUsers = `-- name: GetEventUsers :many -SELECT u.id, u.name, u.email, u.email_verified, u.onboarded, u.image, u.created_at, u.updated_at, u.role, u.preferred_email, u.email_consent, er.role AS event_role -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1 -` - -type GetEventUsersRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Email *string `json:"email"` - EmailVerified bool `json:"email_verified"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Role AuthUserRole `json:"role"` - PreferredEmail *string `json:"preferred_email"` - EmailConsent bool `json:"email_consent"` - EventRole EventRoleType `json:"event_role"` -} - -func (q *Queries) GetEventUsers(ctx context.Context, eventID uuid.UUID) ([]GetEventUsersRow, error) { - rows, err := q.db.Query(ctx, getEventUsers, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetEventUsersRow{} - for rows.Next() { - var i GetEventUsersRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Email, - &i.EmailVerified, - &i.Onboarded, - &i.Image, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.PreferredEmail, - &i.EmailConsent, - &i.EventRole, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getUserByRFID = `-- name: GetUserByRFID :one -SELECT u.id, u.name, u.email, u.email_verified, u.onboarded, u.image, u.created_at, u.updated_at, u.role, u.preferred_email, u.email_consent -FROM auth.users u -JOIN event_roles er ON u.id = er.user_id -WHERE er.event_id = $1 - AND er.rfid = $2 -` - -type GetUserByRFIDParams struct { - EventID uuid.UUID `json:"event_id"` - Rfid *string `json:"rfid"` -} - -func (q *Queries) GetUserByRFID(ctx context.Context, arg GetUserByRFIDParams) (AuthUser, error) { - row := q.db.QueryRow(ctx, getUserByRFID, arg.EventID, arg.Rfid) - var i AuthUser - err := row.Scan( - &i.ID, - &i.Name, - &i.Email, - &i.EmailVerified, - &i.Onboarded, - &i.Image, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.PreferredEmail, - &i.EmailConsent, - ) - return i, err -} - -const removeRole = `-- name: RemoveRole :exec -DELETE FROM event_roles -WHERE event_id = $1 - AND user_id = $2 -` - -type RemoveRoleParams struct { - EventID uuid.UUID `json:"event_id"` - UserID uuid.UUID `json:"user_id"` -} - -func (q *Queries) RemoveRole(ctx context.Context, arg RemoveRoleParams) error { - _, err := q.db.Exec(ctx, removeRole, arg.EventID, arg.UserID) - return err -} - -const updateEventRoleByIds = `-- name: UpdateEventRoleByIds :exec -UPDATE event_roles -SET - role = CASE WHEN $1::boolean THEN $2 ELSE role END, - rfid = CASE WHEN $3::boolean THEN $4 ELSE rfid END, - checked_in_at = CASE WHEN $5::boolean THEN $6 ELSE checked_in_at END -WHERE user_id = $7 - AND event_id = $8 -` - -type UpdateEventRoleByIdsParams struct { - RoleDoUpdate bool `json:"role_do_update"` - Role EventRoleType `json:"role"` - RfidDoUpdate bool `json:"rfid_do_update"` - Rfid *string `json:"rfid"` - CheckedInAtDoUpdate bool `json:"checked_in_at_do_update"` - CheckedInAt *time.Time `json:"checked_in_at"` - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) UpdateEventRoleByIds(ctx context.Context, arg UpdateEventRoleByIdsParams) error { - _, err := q.db.Exec(ctx, updateEventRoleByIds, - arg.RoleDoUpdate, - arg.Role, - arg.RfidDoUpdate, - arg.Rfid, - arg.CheckedInAtDoUpdate, - arg.CheckedInAt, - arg.UserID, - arg.EventID, - ) - return err -} - -const updateRole = `-- name: UpdateRole :exec -UPDATE event_roles -SET role = $3 -WHERE event_id = $1 AND user_id = $2 -` - -type UpdateRoleParams struct { - EventID uuid.UUID `json:"event_id"` - UserID uuid.UUID `json:"user_id"` - Role EventRoleType `json:"role"` -} - -func (q *Queries) UpdateRole(ctx context.Context, arg UpdateRoleParams) error { - _, err := q.db.Exec(ctx, updateRole, arg.EventID, arg.UserID, arg.Role) - return err -} diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go deleted file mode 100644 index 972f044e..00000000 --- a/apps/api/internal/db/sqlc/events.sql.go +++ /dev/null @@ -1,497 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: events.sql - -package sqlc - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const createEvent = `-- name: CreateEvent :one -INSERT INTO events ( - name, - application_open, application_close, - start_time, end_time, - description, location, location_url, max_attendees, - rsvp_deadline, decision_release, - website_url, is_published -) VALUES ( - -- FIXME: The second parameter in coalesce MUST be the default value created in the schema. I have not found a more automated way to insert the default value. - $1, - $2, $3, - $4, $5, - coalesce($6, NULL), - coalesce($7, NULL), - coalesce($8, NULL), - coalesce($9, NULL::INT), - coalesce($10, NULL::TIMESTAMPTZ), - coalesce($11, NULL::TIMESTAMPTZ), - coalesce($12, NULL), - coalesce($13, FALSE) -) -RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at, banner, application_review_started -` - -type CreateEventParams struct { - Name string `json:"name"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - Description interface{} `json:"description"` - Location interface{} `json:"location"` - LocationUrl interface{} `json:"location_url"` - MaxAttendees interface{} `json:"max_attendees"` - RsvpDeadline interface{} `json:"rsvp_deadline"` - DecisionRelease interface{} `json:"decision_release"` - WebsiteUrl interface{} `json:"website_url"` - IsPublished interface{} `json:"is_published"` -} - -func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { - row := q.db.QueryRow(ctx, createEvent, - arg.Name, - arg.ApplicationOpen, - arg.ApplicationClose, - arg.StartTime, - arg.EndTime, - arg.Description, - arg.Location, - arg.LocationUrl, - arg.MaxAttendees, - arg.RsvpDeadline, - arg.DecisionRelease, - arg.WebsiteUrl, - arg.IsPublished, - ) - var i Event - err := row.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - &i.Banner, - &i.ApplicationReviewStarted, - ) - return i, err -} - -const deleteEventById = `-- name: DeleteEventById :execrows -DELETE FROM events -WHERE id = $1 -` - -// execrows returns affect row count on top of an error -func (q *Queries) DeleteEventById(ctx context.Context, id uuid.UUID) (int64, error) { - result, err := q.db.Exec(ctx, deleteEventById, id) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - -const getAllEvents = `-- name: GetAllEvents :many -SELECT - e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, e.banner, e.application_review_started, - er.role AS event_role -FROM events e -LEFT JOIN event_roles AS er - ON er.event_id = e.id - AND er.user_id = $1 -ORDER BY e.start_time ASC -` - -type GetAllEventsRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Banner *string `json:"banner"` - ApplicationReviewStarted bool `json:"application_review_started"` - EventRole NullEventRoleType `json:"event_role"` -} - -func (q *Queries) GetAllEvents(ctx context.Context, userID uuid.UUID) ([]GetAllEventsRow, error) { - rows, err := q.db.Query(ctx, getAllEvents, userID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetAllEventsRow{} - for rows.Next() { - var i GetAllEventsRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - &i.Banner, - &i.ApplicationReviewStarted, - &i.EventRole, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getEventByID = `-- name: GetEventByID :one -SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at, banner, application_review_started FROM events -WHERE id = $1 -` - -func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) { - row := q.db.QueryRow(ctx, getEventByID, id) - var i Event - err := row.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - &i.Banner, - &i.ApplicationReviewStarted, - ) - return i, err -} - -const getEventRoleByIds = `-- name: GetEventRoleByIds :one -SELECT user_id, event_id, role, assigned_at, checked_in_at, rfid FROM event_roles -WHERE user_id = $1::uuid AND event_id = $2::uuid -` - -type GetEventRoleByIdsParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsParams) (EventRole, error) { - row := q.db.QueryRow(ctx, getEventRoleByIds, arg.UserID, arg.EventID) - var i EventRole - err := row.Scan( - &i.UserID, - &i.EventID, - &i.Role, - &i.AssignedAt, - &i.CheckedInAt, - &i.Rfid, - ) - return i, err -} - -const getEventsWithUserInfo = `-- name: GetEventsWithUserInfo :many -SELECT - e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, e.banner, e.application_review_started, - er.role AS event_role, - a.status AS application_status -FROM events e -LEFT JOIN event_roles er - ON er.event_id = e.id - AND er.user_id = $1 -LEFT JOIN applications a - ON a.event_id = e.id - AND a.user_id = $1 -WHERE - CASE $2::get_event_scope_type - WHEN 'all' THEN - TRUE - WHEN 'scoped' THEN - e.is_published = TRUE OR (e.is_published = FALSE AND (er.role = 'staff' or er.role = 'admin')) - ELSE - e.is_published = TRUE - END -ORDER BY e.start_time ASC -` - -type GetEventsWithUserInfoParams struct { - UserID *uuid.UUID `json:"user_id"` - Scope GetEventScopeType `json:"scope"` -} - -type GetEventsWithUserInfoRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Banner *string `json:"banner"` - ApplicationReviewStarted bool `json:"application_review_started"` - EventRole NullEventRoleType `json:"event_role"` - ApplicationStatus NullApplicationStatus `json:"application_status"` -} - -func (q *Queries) GetEventsWithUserInfo(ctx context.Context, arg GetEventsWithUserInfoParams) ([]GetEventsWithUserInfoRow, error) { - rows, err := q.db.Query(ctx, getEventsWithUserInfo, arg.UserID, arg.Scope) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetEventsWithUserInfoRow{} - for rows.Next() { - var i GetEventsWithUserInfoRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - &i.Banner, - &i.ApplicationReviewStarted, - &i.EventRole, - &i.ApplicationStatus, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getPublishedEvents = `-- name: GetPublishedEvents :many -SELECT - e.id, e.name, e.description, e.location, e.location_url, e.max_attendees, e.application_open, e.application_close, e.rsvp_deadline, e.decision_release, e.start_time, e.end_time, e.website_url, e.is_published, e.created_at, e.updated_at, e.banner, e.application_review_started, - er.role AS event_role -FROM events e -LEFT JOIN event_roles AS er - ON er.event_id = e.id - AND er.user_id = $1 -WHERE e.is_published = TRUE -ORDER BY e.start_time ASC -` - -type GetPublishedEventsRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Banner *string `json:"banner"` - ApplicationReviewStarted bool `json:"application_review_started"` - EventRole NullEventRoleType `json:"event_role"` -} - -func (q *Queries) GetPublishedEvents(ctx context.Context, userID uuid.UUID) ([]GetPublishedEventsRow, error) { - rows, err := q.db.Query(ctx, getPublishedEvents, userID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetPublishedEventsRow{} - for rows.Next() { - var i GetPublishedEventsRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.Location, - &i.LocationUrl, - &i.MaxAttendees, - &i.ApplicationOpen, - &i.ApplicationClose, - &i.RsvpDeadline, - &i.DecisionRelease, - &i.StartTime, - &i.EndTime, - &i.WebsiteUrl, - &i.IsPublished, - &i.CreatedAt, - &i.UpdatedAt, - &i.Banner, - &i.ApplicationReviewStarted, - &i.EventRole, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateEventById = `-- name: UpdateEventById :exec -UPDATE events -SET - name = CASE WHEN $1::boolean THEN $2 ELSE name END, - description = CASE WHEN $3::boolean THEN $4 ELSE description END, - location = CASE WHEN $5::boolean THEN $6 ELSE location END, - location_url = CASE WHEN $7::boolean THEN $8 ELSE location_url END, - max_attendees = CASE WHEN $9::boolean THEN $10 ELSE max_attendees END, - application_open = CASE WHEN $11::boolean THEN $12 ELSE application_open END, - application_close = CASE WHEN $13::boolean THEN $14 ELSE application_close END, - rsvp_deadline = CASE WHEN $15::boolean THEN $16 ELSE rsvp_deadline END, - decision_release = CASE WHEN $17::boolean THEN $18 ELSE decision_release END, - start_time = CASE WHEN $19::boolean THEN $20 ELSE start_time END, - end_time = CASE WHEN $21::boolean THEN $22 ELSE end_time END, - website_url = CASE WHEN $23::boolean THEN $24 ELSE website_url END, - is_published = CASE WHEN $25::boolean THEN $26 ELSE is_published END, - banner = CASE WHEN $27::boolean THEN $28 ELSE banner END, - application_review_started = CASE WHEN $29::boolean THEN $30 ELSE application_review_started END -WHERE - id = $31::uuid -RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, created_at, updated_at, banner, application_review_started -` - -type UpdateEventByIdParams struct { - NameDoUpdate bool `json:"name_do_update"` - Name string `json:"name"` - DescriptionDoUpdate bool `json:"description_do_update"` - Description *string `json:"description"` - LocationDoUpdate bool `json:"location_do_update"` - Location *string `json:"location"` - LocationUrlDoUpdate bool `json:"location_url_do_update"` - LocationUrl *string `json:"location_url"` - MaxAttendeesDoUpdate bool `json:"max_attendees_do_update"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpenDoUpdate bool `json:"application_open_do_update"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationCloseDoUpdate bool `json:"application_close_do_update"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadlineDoUpdate bool `json:"rsvp_deadline_do_update"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionReleaseDoUpdate bool `json:"decision_release_do_update"` - DecisionRelease *time.Time `json:"decision_release"` - StartTimeDoUpdate bool `json:"start_time_do_update"` - StartTime time.Time `json:"start_time"` - EndTimeDoUpdate bool `json:"end_time_do_update"` - EndTime time.Time `json:"end_time"` - WebsiteUrlDoUpdate bool `json:"website_url_do_update"` - WebsiteUrl *string `json:"website_url"` - IsPublishedDoUpdate bool `json:"is_published_do_update"` - IsPublished *bool `json:"is_published"` - BannerDoUpdate bool `json:"banner_do_update"` - Banner *string `json:"banner"` - ApplicationReviewStartedDoUpdate bool `json:"application_review_started_do_update"` - ApplicationReviewStarted bool `json:"application_review_started"` - ID uuid.UUID `json:"id"` -} - -func (q *Queries) UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error { - _, err := q.db.Exec(ctx, updateEventById, - arg.NameDoUpdate, - arg.Name, - arg.DescriptionDoUpdate, - arg.Description, - arg.LocationDoUpdate, - arg.Location, - arg.LocationUrlDoUpdate, - arg.LocationUrl, - arg.MaxAttendeesDoUpdate, - arg.MaxAttendees, - arg.ApplicationOpenDoUpdate, - arg.ApplicationOpen, - arg.ApplicationCloseDoUpdate, - arg.ApplicationClose, - arg.RsvpDeadlineDoUpdate, - arg.RsvpDeadline, - arg.DecisionReleaseDoUpdate, - arg.DecisionRelease, - arg.StartTimeDoUpdate, - arg.StartTime, - arg.EndTimeDoUpdate, - arg.EndTime, - arg.WebsiteUrlDoUpdate, - arg.WebsiteUrl, - arg.IsPublishedDoUpdate, - arg.IsPublished, - arg.BannerDoUpdate, - arg.Banner, - arg.ApplicationReviewStartedDoUpdate, - arg.ApplicationReviewStarted, - arg.ID, - ) - return err -} diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go deleted file mode 100644 index 50c6fb57..00000000 --- a/apps/api/internal/db/sqlc/models.go +++ /dev/null @@ -1,481 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 - -package sqlc - -import ( - "database/sql/driver" - "fmt" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" -) - -type ApplicationStatus string - -const ( - ApplicationStatusStarted ApplicationStatus = "started" - ApplicationStatusSubmitted ApplicationStatus = "submitted" - ApplicationStatusUnderReview ApplicationStatus = "under_review" - ApplicationStatusAccepted ApplicationStatus = "accepted" - ApplicationStatusRejected ApplicationStatus = "rejected" - ApplicationStatusWaitlisted ApplicationStatus = "waitlisted" - ApplicationStatusWithdrawn ApplicationStatus = "withdrawn" -) - -func (e *ApplicationStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = ApplicationStatus(s) - case string: - *e = ApplicationStatus(s) - default: - return fmt.Errorf("unsupported scan type for ApplicationStatus: %T", src) - } - return nil -} - -type NullApplicationStatus struct { - ApplicationStatus ApplicationStatus `json:"application_status"` - Valid bool `json:"valid"` // Valid is true if ApplicationStatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullApplicationStatus) Scan(value interface{}) error { - if value == nil { - ns.ApplicationStatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.ApplicationStatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullApplicationStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.ApplicationStatus), nil -} - -type AuthUserRole string - -const ( - AuthUserRoleUser AuthUserRole = "user" - AuthUserRoleSuperuser AuthUserRole = "superuser" -) - -func (e *AuthUserRole) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = AuthUserRole(s) - case string: - *e = AuthUserRole(s) - default: - return fmt.Errorf("unsupported scan type for AuthUserRole: %T", src) - } - return nil -} - -type NullAuthUserRole struct { - AuthUserRole AuthUserRole `json:"auth_user_role"` - Valid bool `json:"valid"` // Valid is true if AuthUserRole is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullAuthUserRole) Scan(value interface{}) error { - if value == nil { - ns.AuthUserRole, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.AuthUserRole.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullAuthUserRole) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.AuthUserRole), nil -} - -type BatRunStatus string - -const ( - BatRunStatusRunning BatRunStatus = "running" - BatRunStatusCompleted BatRunStatus = "completed" - BatRunStatusFailed BatRunStatus = "failed" -) - -func (e *BatRunStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = BatRunStatus(s) - case string: - *e = BatRunStatus(s) - default: - return fmt.Errorf("unsupported scan type for BatRunStatus: %T", src) - } - return nil -} - -type NullBatRunStatus struct { - BatRunStatus BatRunStatus `json:"bat_run_status"` - Valid bool `json:"valid"` // Valid is true if BatRunStatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullBatRunStatus) Scan(value interface{}) error { - if value == nil { - ns.BatRunStatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.BatRunStatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullBatRunStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.BatRunStatus), nil -} - -type EventRoleType string - -const ( - EventRoleTypeAdmin EventRoleType = "admin" - EventRoleTypeStaff EventRoleType = "staff" - EventRoleTypeAttendee EventRoleType = "attendee" - EventRoleTypeApplicant EventRoleType = "applicant" -) - -func (e *EventRoleType) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = EventRoleType(s) - case string: - *e = EventRoleType(s) - default: - return fmt.Errorf("unsupported scan type for EventRoleType: %T", src) - } - return nil -} - -type NullEventRoleType struct { - EventRoleType EventRoleType `json:"event_role_type"` - Valid bool `json:"valid"` // Valid is true if EventRoleType is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullEventRoleType) Scan(value interface{}) error { - if value == nil { - ns.EventRoleType, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.EventRoleType.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullEventRoleType) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.EventRoleType), nil -} - -type GetEventScopeType string - -const ( - GetEventScopeTypePublished GetEventScopeType = "published" - GetEventScopeTypeScoped GetEventScopeType = "scoped" - GetEventScopeTypeAll GetEventScopeType = "all" -) - -func (e *GetEventScopeType) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = GetEventScopeType(s) - case string: - *e = GetEventScopeType(s) - default: - return fmt.Errorf("unsupported scan type for GetEventScopeType: %T", src) - } - return nil -} - -type NullGetEventScopeType struct { - GetEventScopeType GetEventScopeType `json:"get_event_scope_type"` - Valid bool `json:"valid"` // Valid is true if GetEventScopeType is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullGetEventScopeType) Scan(value interface{}) error { - if value == nil { - ns.GetEventScopeType, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.GetEventScopeType.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullGetEventScopeType) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.GetEventScopeType), nil -} - -type InvitationStatus string - -const ( - InvitationStatusPENDING InvitationStatus = "PENDING" - InvitationStatusACCEPTED InvitationStatus = "ACCEPTED" - InvitationStatusEXPIRED InvitationStatus = "EXPIRED" - InvitationStatusREJECTED InvitationStatus = "REJECTED" -) - -func (e *InvitationStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = InvitationStatus(s) - case string: - *e = InvitationStatus(s) - default: - return fmt.Errorf("unsupported scan type for InvitationStatus: %T", src) - } - return nil -} - -type NullInvitationStatus struct { - InvitationStatus InvitationStatus `json:"invitation_status"` - Valid bool `json:"valid"` // Valid is true if InvitationStatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullInvitationStatus) Scan(value interface{}) error { - if value == nil { - ns.InvitationStatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.InvitationStatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullInvitationStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.InvitationStatus), nil -} - -type JoinRequestStatus string - -const ( - JoinRequestStatusPENDING JoinRequestStatus = "PENDING" - JoinRequestStatusAPPROVED JoinRequestStatus = "APPROVED" - JoinRequestStatusREJECTED JoinRequestStatus = "REJECTED" -) - -func (e *JoinRequestStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = JoinRequestStatus(s) - case string: - *e = JoinRequestStatus(s) - default: - return fmt.Errorf("unsupported scan type for JoinRequestStatus: %T", src) - } - return nil -} - -type NullJoinRequestStatus struct { - JoinRequestStatus JoinRequestStatus `json:"join_request_status"` - Valid bool `json:"valid"` // Valid is true if JoinRequestStatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullJoinRequestStatus) Scan(value interface{}) error { - if value == nil { - ns.JoinRequestStatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.JoinRequestStatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullJoinRequestStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.JoinRequestStatus), nil -} - -type Application struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` - Status NullApplicationStatus `json:"status"` - Application []byte `json:"application"` - CreatedAt time.Time `json:"created_at"` - SavedAt time.Time `json:"saved_at"` - UpdatedAt time.Time `json:"updated_at"` - SubmittedAt *time.Time `json:"submitted_at"` - ExperienceRating *int32 `json:"experience_rating"` - PassionRating *int32 `json:"passion_rating"` - AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` - WaitlistJoinTime *time.Time `json:"waitlist_join_time"` -} - -type AuthAccount struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ProviderID string `json:"provider_id"` - AccountID string `json:"account_id"` - HashedPassword *string `json:"hashed_password"` - AccessToken *string `json:"access_token"` - RefreshToken *string `json:"refresh_token"` - IDToken *string `json:"id_token"` - AccessTokenExpiresAt *time.Time `json:"access_token_expires_at"` - RefreshTokenExpiresAt *time.Time `json:"refresh_token_expires_at"` - Scope *string `json:"scope"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type AuthSession struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ExpiresAt time.Time `json:"expires_at"` - IpAddress *string `json:"ip_address"` - UserAgent *string `json:"user_agent"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LastUsedAt time.Time `json:"last_used_at"` -} - -type AuthUser struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Email *string `json:"email"` - EmailVerified bool `json:"email_verified"` - Onboarded bool `json:"onboarded"` - Image *string `json:"image"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Role AuthUserRole `json:"role"` - PreferredEmail *string `json:"preferred_email"` - EmailConsent bool `json:"email_consent"` -} - -type BatRun struct { - ID uuid.UUID `json:"id"` - EventID uuid.UUID `json:"event_id"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` - Status NullBatRunStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - CompletedAt *time.Time `json:"completed_at"` -} - -type Event struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - Location *string `json:"location"` - LocationUrl *string `json:"location_url"` - MaxAttendees *int32 `json:"max_attendees"` - ApplicationOpen time.Time `json:"application_open"` - ApplicationClose time.Time `json:"application_close"` - RsvpDeadline *time.Time `json:"rsvp_deadline"` - DecisionRelease *time.Time `json:"decision_release"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - WebsiteUrl *string `json:"website_url"` - IsPublished *bool `json:"is_published"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Banner *string `json:"banner"` - ApplicationReviewStarted bool `json:"application_review_started"` -} - -type EventInterestSubmission struct { - ID uuid.UUID `json:"id"` - EventID uuid.UUID `json:"event_id"` - Email string `json:"email"` - CreatedAt time.Time `json:"created_at"` - Source *string `json:"source"` -} - -type EventRole struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` - Role EventRoleType `json:"role"` - AssignedAt *time.Time `json:"assigned_at"` - CheckedInAt *time.Time `json:"checked_in_at"` - Rfid *string `json:"rfid"` -} - -type Redeemable struct { - ID uuid.UUID `json:"id"` - EventID uuid.UUID `json:"event_id"` - Name string `json:"name"` - Amount int32 `json:"amount"` - MaxUserAmount int32 `json:"max_user_amount"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` -} - -type Team struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - OwnerID *uuid.UUID `json:"owner_id"` - EventID *uuid.UUID `json:"event_id"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` -} - -type TeamInvitation struct { - ID uuid.UUID `json:"id"` - TeamID uuid.UUID `json:"team_id"` - InvitedByUserID uuid.UUID `json:"invited_by_user_id"` - InvitedEmail string `json:"invited_email"` - InvitedUserID *uuid.UUID `json:"invited_user_id"` - Status InvitationStatus `json:"status"` - ExpiresAt *time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type TeamJoinRequest struct { - ID uuid.UUID `json:"id"` - TeamID uuid.UUID `json:"team_id"` - UserID uuid.UUID `json:"user_id"` - RequestMessage *string `json:"request_message"` - Status JoinRequestStatus `json:"status"` - ProcessedByUserID *uuid.UUID `json:"processed_by_user_id"` - ProcessedAt *time.Time `json:"processed_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type TeamMember struct { - UserID uuid.UUID `json:"user_id"` - TeamID uuid.UUID `json:"team_id"` - JoinedAt *time.Time `json:"joined_at"` -} - -type UserRedemption struct { - UserID uuid.UUID `json:"user_id"` - RedeemableID uuid.UUID `json:"redeemable_id"` - Amount int32 `json:"amount"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` -} diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go deleted file mode 100644 index 6f51c6b0..00000000 --- a/apps/api/internal/db/sqlc/querier.go +++ /dev/null @@ -1,55 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 - -package sqlc - -import ( - "context" - - "github.com/google/uuid" -) - -type Querier interface { - // Adds a new email to the mailing list for a specific user and event. - // The unique constraint on (event_id, user_id) will prevent duplicates. - // Returns the newly created email record. - AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) - AssignRole(ctx context.Context, arg AssignRoleParams) error - CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) - CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) - CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) - CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) - CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) - DeleteAccount(ctx context.Context, arg DeleteAccountParams) error - DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error - // execrows returns affect row count on top of an error - DeleteEventById(ctx context.Context, id uuid.UUID) (int64, error) - DeleteExpiredSession(ctx context.Context) error - DeleteUser(ctx context.Context, id uuid.UUID) error - GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) - GetAllEvents(ctx context.Context, userID uuid.UUID) ([]GetAllEventsRow, error) - GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) - GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) - GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) - GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) - GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsParams) (EventRole, error) - GetEventStaff(ctx context.Context, eventID uuid.UUID) ([]GetEventStaffRow, error) - GetEventsWithUserInfo(ctx context.Context, arg GetEventsWithUserInfoParams) ([]GetEventsWithUserInfoRow, error) - GetPublishedEvents(ctx context.Context, userID uuid.UUID) ([]GetPublishedEventsRow, error) - GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) - GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) - GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) - GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) - GetUsers(ctx context.Context, arg GetUsersParams) ([]AuthUser, error) - InvalidateSessionByID(ctx context.Context, id uuid.UUID) error - TouchSession(ctx context.Context, arg TouchSessionParams) error - UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error - UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error - UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error - UpdateTokens(ctx context.Context, arg UpdateTokensParams) error - UpdateUser(ctx context.Context, arg UpdateUserParams) error - UpdateUserOnboarded(ctx context.Context, id uuid.UUID) error -} - -var _ Querier = (*Queries)(nil) diff --git a/apps/api/internal/domains/application/http.go b/apps/api/internal/domains/application/http.go new file mode 100644 index 00000000..3dc41017 --- /dev/null +++ b/apps/api/internal/domains/application/http.go @@ -0,0 +1,734 @@ +package application + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "strconv" + "time" + + "github.com/danielgtaylor/huma/v2" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/domains/bat" +) + +func RegisterRoutes(applicationHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-application", + Method: http.MethodGet, + Summary: "Get Application", + Description: "Get the application of the current user", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetApplication) + + huma.Register(group, huma.Operation{ + OperationID: "save-application", + Method: http.MethodPost, + Summary: "Save Application", + Description: "Save user's progress on the application. File/Upload fields are not saved (eg. resumes).", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/save", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleSaveApplication) + + huma.Register(group, huma.Operation{ + OperationID: "submit-application", + Method: http.MethodPost, + Summary: "Submit Application", + Description: "Submit the application", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RawHTTPMiddlewareHuma, mw.Auth.RequireAuthHuma}, + Path: "/submit", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleSubmitApplication) + + huma.Register(group, huma.Operation{ + OperationID: "get-download-resume-url", + Method: http.MethodGet, + Summary: "Get Resume Download URL", + Description: "Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/resume", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetDownloadResumeURL) + + huma.Register(group, huma.Operation{ + OperationID: "get-application-statistics", + Method: http.MethodGet, + Summary: "Get Application Statistics", + Description: "Aggregates applications by race, gender, age, majors, and schools", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/stats", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetApplicationStatistics) + + huma.Register(group, huma.Operation{ + OperationID: "submit-application-review", + Method: http.MethodPost, + Summary: "Submit Application Review", + Description: "Handles ratings submissions from staff during the application review process", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review/{applicantId}", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleSubmitApplicationReview) + + huma.Register(group, huma.Operation{ + OperationID: "get-assigned-applications", + Method: http.MethodGet, + Summary: "Get Assigned Applications", + Description: "Returns assigned applications and their review progress for the authenticated reviewer", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/assigned", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetAssignedApplications) + + huma.Register(group, huma.Operation{ + OperationID: "assign-application-reviewers", + Method: http.MethodPost, + Summary: "Assign Application Reviewers", + Description: "Assigns applications to reviewers for the application review process.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/assign", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleAssignApplicationReviewers) + + huma.Register(group, huma.Operation{ + OperationID: "reset-application-reviews", + Method: http.MethodPost, + Summary: "Reset Application Reviews", + Description: "Resets all application reviews, clearing any existing reviewer assignments.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/review/reset", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleResetApplicationReviews) + + huma.Register(group, huma.Operation{ + OperationID: "get-resume", + Method: http.MethodGet, + Summary: "Get Resume URL (for review process)", + Description: "Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Path: "/review/{applicantId}/resume", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleGetResumePresignedUrl) + + huma.Register(group, huma.Operation{ + OperationID: "join-waitlist", + Method: http.MethodPatch, + Summary: "Join Waitlist", + Description: "Adds a waitlist join time to application. Sets status to waitlisted", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/join-waitlist", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleJoinWaitlist) + + huma.Register(group, huma.Operation{ + OperationID: "withdraw-acceptance", + Method: http.MethodPatch, + Summary: "Withdraw Acceptance", + Description: "Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/withdraw-acceptance", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleWithdrawAcceptance) + + huma.Register(group, huma.Operation{ + OperationID: "withdraw-attendance", + Method: http.MethodPatch, + Summary: "Withdraw Attendance", + Description: "Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/withdraw-attendance", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleWithdrawAttendance) + + huma.Register(group, huma.Operation{ + OperationID: "accept-application-acceptance", + Method: http.MethodPatch, + Summary: "Accept Application Acceptance", + Description: "Accept an acceptance after being accepted. Sets event role to attendee, from applicant.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/accept-acceptance", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleAcceptApplicationAcceptance) + + huma.Register(group, huma.Operation{ + OperationID: "transition-waitlist", + Method: http.MethodPatch, + Summary: "Transition Waitlisted Applications", + Description: "Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Path: "/transition-waitlisted-applications", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleTransitionWaitlistedApplications) + + huma.Register(group, huma.Operation{ + OperationID: "calculate-admissions-request", + Method: http.MethodPost, + Summary: "Submit Admissions Calculation Request", + Description: "Queues an admission calculation task to the BAT worker", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/calculate-admissions", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleCalculateAdmissionsRequest) + + huma.Register(group, huma.Operation{ + OperationID: "release-decisions", + Method: http.MethodPost, + Summary: "Release Decisions", + Description: "Releases decisions that were calculated by the worker from a specific run id", + Tags: []string{"Application"}, + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Path: "/release-decisions/{runId}", + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, applicationHandler.handleReleaseDecisions) +} + +type handler struct { + applicationService *ApplicationService + batService *bat.BatService + config *config.Config + logger zerolog.Logger +} + +func NewHandler( + applicationService *ApplicationService, batService *bat.BatService, + config *config.Config, logger zerolog.Logger, +) *handler { + return &handler{ + applicationService: applicationService, + batService: batService, + config: config, + logger: logger, + } +} + +type HackerApplication struct { + UserID uuid.UUID `json:"userId"` + Status sqlc.ApplicationStatus `json:"status"` + Application []byte `json:"application"` + CreatedAt time.Time `json:"createdAt"` + SavedAt time.Time `json:"savedAt"` + UpdatedAt time.Time `json:"updatedAt"` + SubmittedAt *time.Time `json:"submittedAt"` + HackathonID string `json:"hackathonId"` +} + +type GetApplicationOutput struct { + Body HackerApplication +} + +func (h *handler) handleGetApplication(ctx context.Context, input *struct{}) (*GetApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + application, err := h.applicationService.GetApplicationByUserId(ctx, userCtx.UserID) + if err != nil { + if errors.Is(err, database.ErrApplicationNotFound) { + newApplication, err := h.applicationService.CreateApplication(ctx, userCtx.UserID) + + if err != nil || newApplication == nil { + return nil, huma.Error500InternalServerError("can't create application") + } + + return &GetApplicationOutput{Body: HackerApplication{ + UserID: newApplication.UserID, + Application: newApplication.Application, + CreatedAt: newApplication.CreatedAt, + SavedAt: newApplication.SavedAt, + UpdatedAt: newApplication.UpdatedAt, + SubmittedAt: newApplication.SubmittedAt, + HackathonID: newApplication.HackathonID, + }}, nil + } + if errors.Is(err, ErrApplicationNotOpened) { + return nil, huma.Error400BadRequest("application is unavailable") + } + + return nil, huma.Error500InternalServerError("error retrieving application") + } + + return &GetApplicationOutput{Body: HackerApplication{ + UserID: application.UserID, + Application: application.Application, + CreatedAt: application.CreatedAt, + SavedAt: application.SavedAt, + UpdatedAt: application.UpdatedAt, + SubmittedAt: application.SubmittedAt, + HackathonID: application.HackathonID, + }}, nil +} + +type SaveApplicationOutput struct { + Status int +} + +func (h *handler) handleSaveApplication(ctx context.Context, input *struct { + Body any +}) (*SaveApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + // TODO: validate input.Body, make sure that the data is the application + err := h.applicationService.SaveApplication(ctx, input.Body, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to save application") + } + + return &SaveApplicationOutput{Status: http.StatusOK}, nil +} + +type SubmitApplicationOutput struct { + Status int +} + +func (h *handler) handleSubmitApplication(ctx context.Context, input *struct{}) (*SubmitApplicationOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + r := ctx.Value(middleware.RawRequestKey{}).(*http.Request) + + // Parse multipart form (10 MB max memory) + err := r.ParseMultipartForm(10 << 20) + if err != nil { + return nil, huma.Error400BadRequest("Failed to parse form") + } + + var submission ApplicationSubmissionFields + + // Map form values + submission.FirstName = r.FormValue("firstName") + submission.LastName = r.FormValue("lastName") + + if ageStr := r.FormValue("age"); ageStr != "" { + if age, err := strconv.Atoi(ageStr); err == nil { + submission.Age = age + } + } + + submission.Phone = r.FormValue("phone") + submission.PreferredEmail = r.FormValue("preferredEmail") + submission.UniversityEmail = r.FormValue("universityEmail") + + submission.Country = r.FormValue("country") + submission.Gender = r.FormValue("gender") + submission.GenderOther = r.FormValue("gender-other") + submission.Pronouns = r.FormValue("pronouns") + submission.Race = r.FormValue("race") + submission.RaceOther = r.FormValue("race-other") + submission.Orientation = r.FormValue("orientation") + + submission.Linkedin = r.FormValue("linkedin") + submission.Github = r.FormValue("github") + + if ageCertStr := r.FormValue("ageCertification"); ageCertStr != "" { + submission.AgeCertification = (ageCertStr == "true" || ageCertStr == "1") + } + + submission.School = r.FormValue("school") + submission.Level = r.FormValue("level") + submission.LevelOther = r.FormValue("level-other") + submission.Year = r.FormValue("year") + submission.YearOther = r.FormValue("year-other") + submission.GraduationYear = r.FormValue("graduationYear") + submission.Majors = r.FormValue("majors") + submission.Minors = r.FormValue("minors") + submission.Experience = r.FormValue("experience") + submission.UfHackathonExp = r.FormValue("ufHackathonExp") + submission.ProjectExperience = r.FormValue("projectExperience") + submission.ShirtSize = r.FormValue("shirtSize") + submission.Diet = r.FormValue("diet") + submission.Essay1 = r.FormValue("essay1") + submission.Essay2 = r.FormValue("essay2") + submission.Referral = r.FormValue("referral") + submission.PictureConsent = r.FormValue("pictureConsent") + submission.InPersonAcknowledgement = r.FormValue("inpersonAcknowledgement") + submission.AgreeToConduct = r.FormValue("agreeToConduct") + submission.InfoShareAuthorization = r.FormValue("infoShareAuthorization") + submission.AgreeToMLHEmails = r.FormValue("agreeToMLHEmails") + + resumeFile, _, err := r.FormFile("resume[]") + if err != nil { + return nil, huma.Error400BadRequest("Invalid resume file") + } + + defer resumeFile.Close() + + resumeFileBuffer := bytes.NewBuffer(nil) + + if _, err := io.Copy(resumeFileBuffer, resumeFile); err != nil { + return nil, huma.Error500InternalServerError("Error while parsing resume") + } + + validate := validator.New() + if err := validate.Struct(submission); err != nil { + return nil, huma.Error400BadRequest("Unable to parse application submission") + } + + err = h.applicationService.SubmitApplication(r.Context(), submission, resumeFileBuffer.Bytes(), userCtx.UserID) + + if err != nil { + if errors.Is(err, ErrApplicationNotOpened) { + return nil, huma.Error400BadRequest("Application is not opened") + } + + return nil, huma.Error500InternalServerError("Fail to submit application") + } + + return &SubmitApplicationOutput{Status: http.StatusOK}, nil +} + +type GetDownloadResumeOutput struct { + Body string +} + +func (h *handler) handleGetDownloadResumeURL(ctx context.Context, input *struct{}) (*GetDownloadResumeOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + request, err := h.applicationService.GetDownloadResumeURL(ctx, userCtx.UserID, 60) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get url to download resume") + } + + return &GetDownloadResumeOutput{Body: request.URL}, nil +} + +type GetApplicationStatisticsOutput struct { + Body *ApplicationStatistics +} + +func (h *handler) handleGetApplicationStatistics(ctx context.Context, input *struct{}) (*GetApplicationStatisticsOutput, error) { + stats, err := h.applicationService.GetApplicationStatistics(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get application statistics") + } + + return &GetApplicationStatisticsOutput{Body: stats}, nil +} + +type ReviewRatings struct { + PassionRating int `json:"passionRating" minLength:"1" maxLength:"5" required:"true"` + ExperienceRating int `json:"experienceRating" minLength:"1" maxLength:"5" required:"true"` +} + +type SubmitApplicationReviewOutput struct { + Status int +} + +func (h *handler) handleSubmitApplicationReview(ctx context.Context, input *struct { + ApplicantId string `path:"applicantId"` + Body ReviewRatings +}) (*SubmitApplicationReviewOutput, error) { + applicantId, err := uuid.Parse(input.ApplicantId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid applicant id") + } + + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err = h.applicationService.SaveApplicationReview(ctx, userCtx.UserID, applicantId, input.Body.ExperienceRating, input.Body.PassionRating) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to save review") + } + + return &SubmitApplicationReviewOutput{Status: http.StatusCreated}, nil +} + +type GetAssignedApplicationsOutput struct { + Body []AssignedApplication +} + +func (h *handler) handleGetAssignedApplications(ctx context.Context, input *struct{}) (*GetAssignedApplicationsOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + applications, err := h.applicationService.GetAssignedApplicationsAndProgress(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to get assigned applications") + } + + return &GetAssignedApplicationsOutput{Body: applications}, nil +} + +type AssignApplicationReviewersOutput struct { + Status int +} + +func (h *handler) handleAssignApplicationReviewers(ctx context.Context, input *struct { + Body []ReviewerAssignment +}) (*AssignApplicationReviewersOutput, error) { + err := h.applicationService.AssignReviewers(ctx, input.Body) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to assign reviewers") + } + + return &AssignApplicationReviewersOutput{Status: http.StatusOK}, nil +} + +type ResetApplicationReviewsOutput struct { + Status int +} + +func (h *handler) handleResetApplicationReviews(ctx context.Context, input *struct{}) (*ResetApplicationReviewsOutput, error) { + err := h.applicationService.ResetApplicationReviews(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to reset application reviews") + } + + return &ResetApplicationReviewsOutput{Status: http.StatusOK}, nil +} + +type GetResumePresignedUrlOutput struct { + Body string +} + +func (h *handler) handleGetResumePresignedUrl(ctx context.Context, input *struct { + ApplicantId string `path:"applicantId"` +}) (*GetResumePresignedUrlOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + applicantId, err := uuid.Parse(input.ApplicantId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid applicant id") + } + + if userCtx.Role != sqlc.UserRoleStaff && userCtx.Role != sqlc.UserRoleAdmin && userCtx.UserID != applicantId { + return nil, huma.Error400BadRequest("You are not allowed to see other ppls resumes :(") + } + + request, err := h.applicationService.GetDownloadResumeURL(ctx, applicantId, 600) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to retrieve download url") + } + + return &GetResumePresignedUrlOutput{Body: request.URL}, nil +} + +type JoinWaitlistOutput struct { + Status int +} + +func (h *handler) handleJoinWaitlist(ctx context.Context, input *struct{}) (*JoinWaitlistOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.JoinWaitlist(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to join waitlist") + } + + return &JoinWaitlistOutput{Status: http.StatusOK}, nil +} + +type WithdrawAcceptanceOutput struct { + Status int +} + +func (h *handler) handleWithdrawAcceptance(ctx context.Context, input *struct{}) (*WithdrawAcceptanceOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.WithdrawAcceptance(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to withdraw acceptance") + } + + return &WithdrawAcceptanceOutput{Status: http.StatusOK}, nil +} + +type WithdrawAttendanceOutput struct { + Status int +} + +func (h *handler) handleWithdrawAttendance(ctx context.Context, input *struct{}) (*WithdrawAttendanceOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.WithdrawAttendance(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to withdraw attendance") + } + + return &WithdrawAttendanceOutput{Status: http.StatusOK}, nil +} + +type AcceptApplicationAcceptanceOutput struct { + Status int +} + +func (h *handler) handleAcceptApplicationAcceptance(ctx context.Context, input *struct{}) (*AcceptApplicationAcceptanceOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err := h.applicationService.AcceptApplicationAcceptance(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Unable to withdraw attendance") + } + + return &AcceptApplicationAcceptanceOutput{Status: http.StatusOK}, nil +} + +type TransitionWaitlistedApplicationsOutput struct { + Status int +} + +func (h *handler) handleTransitionWaitlistedApplications(ctx context.Context, input *struct{}) (*TransitionWaitlistedApplicationsOutput, error) { + // TODO: move these numbers elsewhere to a config file or something + var acceptanceCount uint32 = 50 + var acceptanceQuota uint32 = 500 + err := h.applicationService.TransitionWaitlistedApplications(ctx, acceptanceCount, acceptanceQuota) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to transition waitlisted applications") + } + + return &TransitionWaitlistedApplicationsOutput{Status: http.StatusOK}, nil +} + +type CalculateAdmissionsRequestOutput struct { + Status int +} + +func (h *handler) handleCalculateAdmissionsRequest(ctx context.Context, input *struct{}) (*CalculateAdmissionsRequestOutput, error) { + _, err := h.batService.QueueCalculateAdmissionsTask(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to handle admissions calculation request") + } + + return &CalculateAdmissionsRequestOutput{Status: http.StatusOK}, nil +} + +type ReleaseDecisionsOutput struct { + Status int +} + +func (h *handler) handleReleaseDecisions(ctx context.Context, input *struct { + RunId string `path:"runId"` +}) (*ReleaseDecisionsOutput, error) { + runId, err := uuid.Parse(input.RunId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid Run Id") + } + + err = h.applicationService.ReleaseDecisions(ctx, runId) + + if err != nil { + return nil, err + } + + return &ReleaseDecisionsOutput{Status: http.StatusOK}, nil + +} diff --git a/apps/api/internal/services/application.go b/apps/api/internal/domains/application/service.go similarity index 55% rename from apps/api/internal/services/application.go rename to apps/api/internal/domains/application/service.go index 159d0b59..cfd2cf77 100644 --- a/apps/api/internal/services/application.go +++ b/apps/api/internal/domains/application/service.go @@ -1,7 +1,8 @@ -package services +package application import ( "context" + "encoding/json" "errors" "time" @@ -10,21 +11,97 @@ import ( "github.com/jackc/pgx/v5" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/ptr" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/domains/bat" + "github.com/swamphacks/core/apps/api/internal/domains/email" "github.com/swamphacks/core/apps/api/internal/storage" "golang.org/x/sync/errgroup" ) var ( - ErrGetApplicationStatistics = errors.New("failed to aggregate application stats") - ErrMismatchedReviewerCounts = errors.New("the total number of applications does not match the total number of assigned reviews") - ErrWrongReviewerAssignment = errors.New("an application has been assigned to a reviewer who is not authorized to review it") - ErrEventAlreadyStarted = errors.New("the event has already started") + ErrApplicationNotOpened = errors.New("Application not opened") ) +type ApplicationService struct { + userRepo *repository.UserRepository + applicationRepo *repository.ApplicationRepository + hackathonRepo *repository.HackathonRepository + storage storage.Storage + buckets *config.CoreBuckets + txm *database.TransactionManager + scheduler *asynq.Scheduler + emailService *email.EmailService + batService *bat.BatService + config *config.Config + logger zerolog.Logger +} + +func NewService( + applicationRepo *repository.ApplicationRepository, userRepo *repository.UserRepository, + hackathonRepo *repository.HackathonRepository, txm *database.TransactionManager, storage storage.Storage, buckets *config.CoreBuckets, + scheduler *asynq.Scheduler, emailService *email.EmailService, batService *bat.BatService, config *config.Config, logger zerolog.Logger, +) *ApplicationService { + return &ApplicationService{ + applicationRepo: applicationRepo, + userRepo: userRepo, + hackathonRepo: hackathonRepo, + emailService: emailService, // TODO: is there anyway to structure this? I don't know if it's a good idea to depend on another service + batService: batService, + storage: storage, + buckets: buckets, + txm: txm, + scheduler: scheduler, + config: config, + logger: logger.With().Str("service", "ApplicationService").Str("domain", "application").Logger(), + } +} + +func (s *ApplicationService) CreateApplication(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { + hackathon, err := s.hackathonRepo.GetHackathon(ctx) + + if err != nil { + s.logger.Err(err).Msg("Create application fail because can't retrieve hackathon") + return nil, err + } + + now := time.Now() + isApplicationOpen := now.After(hackathon.ApplicationOpen) && now.Before(hackathon.ApplicationClose) + + if !isApplicationOpen { + return nil, ErrApplicationNotOpened + } + + // TODO: don't hardcode the hackathonId + application, err := s.applicationRepo.CreateApplication(ctx, sqlc.CreateApplicationParams{ + UserID: userID, + HackathonID: "xii", + }) + + if err != nil { + s.logger.Err(err).Msg(err.Error()) + return nil, err + } + + return application, nil +} + +func (s *ApplicationService) GetApplicationByUserId(ctx context.Context, userID uuid.UUID) (*sqlc.Application, error) { + application, err := s.applicationRepo.GetApplicationByUserId(ctx, userID) + + if err != nil { + if errors.Is(err, database.ErrApplicationNotFound) { + return nil, database.ErrApplicationNotFound + } else { + s.logger.Err(err).Msg("") + return nil, err + } + } + + return application, nil +} + // TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. // these fields are only applicable to swamphacks xi, not other events type ApplicationSubmissionFields struct { @@ -67,87 +144,42 @@ type ApplicationSubmissionFields struct { AgreeToMLHEmails string `json:"agreeToMLHEmails"` } -var ( - ErrApplicationDeadlinePassed = errors.New("the application deadline has passed") - ErrApplicationUnavailable = errors.New("unable to access the application") - ErrApplicationCannotSave = errors.New("unable to save the application") - ErrApplicationPastSubmitted = errors.New("application has already been submitted and cannot be modified") -) - -type ApplicationService struct { - appRepo *repository.ApplicationRepository - userRepo *repository.UserRepository - eventsService *EventService - emailService *EmailService - storage storage.Storage - buckets *config.CoreBuckets - txm *db.TransactionManager - scheduler *asynq.Scheduler - logger zerolog.Logger -} - -func NewApplicationService(appRepo *repository.ApplicationRepository, userRepo *repository.UserRepository, eventsService *EventService, emailService *EmailService, txm *db.TransactionManager, storage storage.Storage, buckets *config.CoreBuckets, scheduler *asynq.Scheduler, logger zerolog.Logger) *ApplicationService { - return &ApplicationService{ - appRepo: appRepo, - userRepo: userRepo, - eventsService: eventsService, - emailService: emailService, - storage: storage, - buckets: buckets, - txm: txm, - scheduler: scheduler, - logger: logger.With().Str("component", "applicationService").Logger(), - } -} - -func (s *ApplicationService) GetApplicationByUserAndEventID(ctx context.Context, params sqlc.GetApplicationByUserAndEventIDParams) (*sqlc.Application, error) { - application, err := s.appRepo.GetApplicationByUserAndEventID(ctx, params) - - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return nil, err - } - - return application, nil -} - -func (s *ApplicationService) CreateApplication(ctx context.Context, params sqlc.CreateApplicationParams) (*sqlc.Application, error) { - canCreateApplication, err := s.eventsService.IsApplicationsOpen(ctx, params.EventID) +func (s *ApplicationService) SubmitApplication(ctx context.Context, data ApplicationSubmissionFields, resume []byte, userID uuid.UUID) error { + hackathon, err := s.hackathonRepo.GetHackathon(ctx) if err != nil { - return nil, err - } - - if !canCreateApplication { - return nil, nil + s.logger.Err(err).Msg("Submit application fail because can't retrieve hackathon") + return err } - application, err := s.appRepo.CreateApplication(ctx, params) + now := time.Now() + isApplicationOpen := now.After(hackathon.ApplicationOpen) && now.Before(hackathon.ApplicationClose) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return nil, err + if !isApplicationOpen { + return ErrApplicationNotOpened } - return application, nil -} - -func (s *ApplicationService) SubmitApplication(ctx context.Context, data ApplicationSubmissionFields, resume []byte, userId uuid.UUID, eventId uuid.UUID) error { - canSubmitApplication, err := s.eventsService.IsApplicationsOpen(ctx, eventId) + dataJSON, err := json.Marshal(data) if err != nil { - return err - } - - if !canSubmitApplication { - return ErrApplicationDeadlinePassed + return errors.New("Failed to parse application data") } // Submitting application is an atomic operation err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) - - err := txAppRepo.SubmitApplication(ctx, data, userId, eventId) + txAppRepo := s.applicationRepo.NewTx(tx) + + err := txAppRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + StatusDoUpdate: true, + Status: sqlc.ApplicationStatusSubmitted, + ApplicationDoUpdate: true, + Application: dataJSON, + SubmittedAtDoUpdate: true, + SubmittedAt: time.Now(), + SavedAtDoUpdate: true, + SavedAt: time.Now(), + UserID: userID, + }) if err != nil { s.logger.Err(err).Msg(err.Error()) @@ -155,16 +187,19 @@ func (s *ApplicationService) SubmitApplication(ctx context.Context, data Applica } contentType := "application/pdf" - err = s.storage.Store(ctx, s.buckets.ApplicationResumes, eventId.String()+"/"+userId.String(), resume, &contentType) + err = s.storage.Store(ctx, s.buckets.ApplicationResumes, userID.String(), resume, &contentType) if err != nil { s.logger.Err(err).Msg(err.Error()) return err } - err = s.eventsService.AssignEventRole(ctx, ptr.UUIDToPtr(userId), nil, eventId, sqlc.EventRoleTypeApplicant) + err = s.userRepo.UpdateRole(ctx, sqlc.UpdateRoleParams{ + UserID: userID, + Role: sqlc.UserRoleApplicant, + }) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("submit application assign role fail") return err } @@ -181,40 +216,46 @@ func (s *ApplicationService) SubmitApplication(ctx context.Context, data Applica return nil } -func (s *ApplicationService) SaveApplication(ctx context.Context, data any, userId, eventId uuid.UUID) error { +func (s *ApplicationService) SaveApplication(ctx context.Context, data any, userID uuid.UUID) error { // Guard clauses to ensure application can be saved // 1) Check if applications are open for the event // 2) Ensure application status is "started" (Reject all other statuses) - canSaveApplication, err := s.eventsService.IsApplicationsOpen(ctx, eventId) - if err != nil { - return err + if err := s.isApplicationOpen(ctx); err != nil { + return ErrApplicationNotOpened } - if !canSaveApplication { - return ErrApplicationCannotSave - } - - application, err := s.GetApplicationByUserAndEventID(ctx, sqlc.GetApplicationByUserAndEventIDParams{ - UserID: userId, - EventID: eventId, - }) + application, err := s.GetApplicationByUserId(ctx, userID) if err != nil { return err } // This check should almost never fail, but just in case if application == nil { - return ErrApplicationUnavailable + return errors.New("Application not found when saving the application") } - if application.Status.ApplicationStatus != sqlc.ApplicationStatusStarted { - return ErrApplicationPastSubmitted + if application.Status != sqlc.ApplicationStatusStarted { + return errors.New("application has already been submitted and cannot be modified") } - err = s.appRepo.SaveApplication(ctx, data, userId, eventId) + dataJSON, err := json.Marshal(data) if err != nil { - s.logger.Err(err).Msg(err.Error()) + return errors.New("Failed to parse application data") + } + + err = s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + StatusDoUpdate: true, + Status: sqlc.ApplicationStatusStarted, + ApplicationDoUpdate: true, + Application: dataJSON, + UserID: userID, + SavedAtDoUpdate: true, + SavedAt: time.Now(), + }) + + if err != nil { + s.logger.Err(err).Msg("Save application fail") return err } @@ -232,12 +273,12 @@ func (s *ApplicationService) SubmitApplicationReview(ctx context.Context) (*sqlc return nil, nil } -func (s *ApplicationService) DownloadResume(ctx context.Context, userId, eventId uuid.UUID, lifetimeSecs int64) (*storage.PresignedRequest, error) { +func (s *ApplicationService) GetDownloadResumeURL(ctx context.Context, userID uuid.UUID, lifetimeSecs int64) (*storage.PresignedRequest, error) { presignableStorage, ok := s.storage.(storage.PresignableStorage) if !ok { err := errors.New("unable to type cast `Storage` to `PresignableStorage`") - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("download resume fail storage setup") return nil, err } @@ -246,10 +287,10 @@ func (s *ApplicationService) DownloadResume(ctx context.Context, userId, eventId return nil, err } - request, err := presignableStorage.PresignGetObject(ctx, s.buckets.ApplicationResumes, eventId.String()+"/"+userId.String(), lifetimeSecs) + request, err := presignableStorage.PresignGetObject(ctx, s.buckets.ApplicationResumes, userID.String(), lifetimeSecs) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("fail presign get object") return nil, err } @@ -257,15 +298,15 @@ func (s *ApplicationService) DownloadResume(ctx context.Context, userId, eventId } type ApplicationStatistics struct { - GenderStatistics sqlc.GetApplicationGenderSplitRow `json:"gender_stats"` - AgeStatistics sqlc.GetApplicationAgeSplitRow `json:"age_stats"` - RaceStatistics []sqlc.GetApplicationRaceSplitRow `json:"race_stats"` - MajorStatistics []sqlc.GetApplicationMajorSplitRow `json:"major_stats"` - SchoolStatistics []sqlc.GetApplicationSchoolSplitRow `json:"school_stats"` - StatusStatistics sqlc.GetApplicationStatusSplitRow `json:"status_stats"` + GenderStatistics sqlc.GetApplicationGenderSplitRow `json:"genderStats"` + AgeStatistics sqlc.GetApplicationAgeSplitRow `json:"ageStats"` + RaceStatistics []sqlc.GetApplicationRaceSplitRow `json:"raceStats"` + MajorStatistics []sqlc.GetApplicationMajorSplitRow `json:"majorStats"` + SchoolStatistics []sqlc.GetApplicationSchoolSplitRow `json:"schoolStats"` + StatusStatistics sqlc.GetApplicationStatusSplitRow `json:"statusStats"` } -func (s *ApplicationService) GetApplicationStatistics(ctx context.Context, eventId uuid.UUID) (*ApplicationStatistics, error) { +func (s *ApplicationService) GetApplicationStatistics(ctx context.Context) (*ApplicationStatistics, error) { g, ctx := errgroup.WithContext(ctx) var genderStats sqlc.GetApplicationGenderSplitRow @@ -277,43 +318,43 @@ func (s *ApplicationService) GetApplicationStatistics(ctx context.Context, event g.Go(func() error { var err error - genderStats, err = s.appRepo.GetSubmittedApplicationGenders(ctx, eventId) + genderStats, err = s.applicationRepo.GetSubmittedApplicationGenders(ctx) return err }) g.Go(func() error { var err error - ageStats, err = s.appRepo.GetSubmittedApplicationAges(ctx, eventId) + ageStats, err = s.applicationRepo.GetSubmittedApplicationAges(ctx) return err }) g.Go(func() error { var err error - majorStats, err = s.appRepo.GetSubmittedApplicationMajors(ctx, eventId) + majorStats, err = s.applicationRepo.GetSubmittedApplicationMajors(ctx) return err }) g.Go(func() error { var err error - raceStats, err = s.appRepo.GetSubmittedApplicationRaces(ctx, eventId) + raceStats, err = s.applicationRepo.GetSubmittedApplicationRaces(ctx) return err }) g.Go(func() error { var err error - schoolStats, err = s.appRepo.GetSubmittedApplicationSchools(ctx, eventId) + schoolStats, err = s.applicationRepo.GetSubmittedApplicationSchools(ctx) return err }) g.Go(func() error { var err error - statusStats, err = s.appRepo.GetApplicationStatuses(ctx, eventId) + statusStats, err = s.applicationRepo.GetApplicationStatuses(ctx) return err }) if err := g.Wait(); err != nil { s.logger.Err(err).Msg("Something went wrong while getting application statistics") - return nil, ErrGetApplicationStatistics + return nil, errors.New("Get application stats error") } return &ApplicationStatistics{ @@ -328,16 +369,16 @@ func (s *ApplicationService) GetApplicationStatistics(ctx context.Context, event } type ReviewerAssignment struct { - ID uuid.UUID `json:"id"` // User/Reviewer ID + ID uuid.UUID `json:"userID"` // User/Reviewer ID Amount *int `json:"amount"` // Number of applications assigned (nil if autoassign) } type ReviewerAllocation struct { - ReviewerID uuid.UUID `json:"reviewer_id"` - AssignedApplicationIDs []uuid.UUID `json:"assigned_application_ids"` + ReviewerID uuid.UUID `json:"reviewerIdd"` + AssignedApplicationIDs []uuid.UUID `json:"assignedApplicationIds"` } -func (s *ApplicationService) AssignReviewers(ctx context.Context, eventId uuid.UUID, reviewers []ReviewerAssignment) error { +func (s *ApplicationService) AssignReviewers(ctx context.Context, reviewers []ReviewerAssignment) error { //TODO: Must check if applications are closed, if we havent released decisions, and more. @@ -354,7 +395,7 @@ func (s *ApplicationService) AssignReviewers(ctx context.Context, eventId uuid.U } } - availableApplications, err := s.appRepo.ListAvailableApplicationForEvent(ctx, eventId) + availableApplications, err := s.applicationRepo.ListAvailableApplications(ctx) if err != nil { return err } @@ -365,11 +406,11 @@ func (s *ApplicationService) AssignReviewers(ctx context.Context, eventId uuid.U } if totalFixedAmount > totalAvailable { - return ErrMismatchedReviewerCounts + return errors.New("the total number of applications does not match the total number of assigned reviews") } if totalAvailable > totalFixedAmount && len(autoReviewers) == 0 { - return ErrMismatchedReviewerCounts + return errors.New("the total number of applications does not match the total number of assigned reviews") } var appIndex int = 0 @@ -413,38 +454,40 @@ func (s *ApplicationService) AssignReviewers(ctx context.Context, eventId uuid.U } return s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) - txEventRepo := s.eventsService.eventRepo.NewTx(tx) + txAppRepo := s.applicationRepo.NewTx(tx) + txHackathonRepo := s.hackathonRepo.NewTx(tx) for _, allocation := range finalAllocations { - err := txAppRepo.AssignApplicationToReviewByEvent(ctx, allocation.ReviewerID, eventId, allocation.AssignedApplicationIDs) + err := txAppRepo.AssignApplicationToReview(ctx, sqlc.AssignApplicationsToReviewerParams{ + ReviewerID: allocation.ReviewerID, + ApplicationIds: allocation.AssignedApplicationIDs, + }) + if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("assign applicattion to review fail while allocating") return err } } - return txEventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{ - ID: eventId, + return txHackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ ApplicationReviewStartedDoUpdate: true, ApplicationReviewStarted: true, }) }) } -func (s *ApplicationService) ResetApplicationReviews(ctx context.Context, eventId uuid.UUID) error { +func (s *ApplicationService) ResetApplicationReviews(ctx context.Context) error { return s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) - txEventRepo := s.eventsService.eventRepo.NewTx(tx) + txAppRepo := s.applicationRepo.NewTx(tx) + txHackathonRepo := s.hackathonRepo.NewTx(tx) - err := txAppRepo.ResetApplicationReviewsForEvent(ctx, eventId) + err := txAppRepo.ResetApplicationReviews(ctx) if err != nil { s.logger.Err(err).Msg(err.Error()) return err } - return txEventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{ - ID: eventId, + return txHackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ ApplicationReviewStartedDoUpdate: true, ApplicationReviewStarted: false, }) @@ -460,14 +503,14 @@ const ( ) type AssignedApplication struct { - UserID uuid.UUID `json:"user_id"` + UserID uuid.UUID `json:"applicantId"` Status ApplicationReviewStatus `json:"status"` } -func (s *ApplicationService) GetAssignedApplicationsAndProgress(ctx context.Context, reviewerId, eventId uuid.UUID) ([]AssignedApplication, error) { - applications, err := s.appRepo.ListApplicationByReviewerAndEvent(ctx, reviewerId, eventId) +func (s *ApplicationService) GetAssignedApplicationsAndProgress(ctx context.Context, reviewerId uuid.UUID) ([]AssignedApplication, error) { + applications, err := s.applicationRepo.ListApplicationByReviewer(ctx, reviewerId) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("get assigned applications and progress fail because list application by reviewer failed") return nil, err } @@ -487,17 +530,14 @@ func (s *ApplicationService) GetAssignedApplicationsAndProgress(ctx context.Cont return assignedApps, nil } -func (s *ApplicationService) SaveApplicationReview(ctx context.Context, reviewerId, userId, eventId uuid.UUID, experienceRating, passionRating int) error { +func (s *ApplicationService) SaveApplicationReview(ctx context.Context, reviewerId, applicantId uuid.UUID, experienceRating, passionRating int) error { // Log everything for debug - s.logger.Debug().Str("ReviewerId", reviewerId.String()).Str("UserId", userId.String()).Str("eventId", eventId.String()).Int32("Passion Rating", int32(passionRating)).Int32("Experiene Rating", int32(experienceRating)).Msg("Saving app review.") + s.logger.Debug().Str("ReviewerId", reviewerId.String()).Str("ApplicantId", applicantId.String()).Int32("Passion Rating", int32(passionRating)).Int32("Experiene Rating", int32(experienceRating)).Msg("Saving app review.") // Get the assigned application - application, err := s.appRepo.GetApplicationByUserAndEventID(ctx, sqlc.GetApplicationByUserAndEventIDParams{ - UserID: userId, - EventID: eventId, - }) + application, err := s.applicationRepo.GetApplicationByUserId(ctx, applicantId) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("SaveApplicationReview fail, unable to get application for user") return err } @@ -508,12 +548,11 @@ func (s *ApplicationService) SaveApplicationReview(ctx context.Context, reviewer Str("AssignedReviewID", application.AssignedReviewerID.String()). Str("ReviewID", reviewerId.String()). Msg("Cannot review this application. either the assigned review is different or is nil.") - return ErrWrongReviewerAssignment + return errors.New("an application has been assigned to a reviewer who is not authorized to review it") } - if err = s.appRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: userId, - EventID: eventId, + if err = s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + UserID: applicantId, ExperienceRatingDoUpdate: true, ExperienceRating: int32(experienceRating), PassionRatingDoUpdate: true, @@ -529,96 +568,108 @@ func (s *ApplicationService) SaveApplicationReview(ctx context.Context, reviewer return nil } -func (s *ApplicationService) JoinWaitlist(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { - err := s.appRepo.JoinWaitlist(ctx, userId, eventId) +func (s *ApplicationService) CheckApplicationReviewsComplete(ctx context.Context) (bool, error) { + nonReviewedApplicantUUIDs, err := s.applicationRepo.GetNonReviewedApplications(ctx) if err != nil { - s.logger.Err(err).Msg(err.Error()) + return false, errors.New("Failed to check application reviews status") + } + + return len(nonReviewedApplicantUUIDs) == 0, nil +} + +func (s *ApplicationService) JoinWaitlist(ctx context.Context, userID uuid.UUID) error { + err := s.applicationRepo.JoinWaitlist(ctx, userID) + if err != nil { + s.logger.Err(err).Msg("Join waitlist fail") return err } return nil } -func (s *ApplicationService) WithdrawAcceptance(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { - err := s.appRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: userId, - EventID: eventId, - //TODO: Make it so I don't have to set this! +func (s *ApplicationService) WithdrawAcceptance(ctx context.Context, userID uuid.UUID) error { + err := s.applicationRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + UserID: userID, StatusDoUpdate: true, Status: sqlc.ApplicationStatusWithdrawn, }) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("WithdrawAcceptance fail, unable to update application") return err } return nil } -func (s *ApplicationService) WithdrawAttendance(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { +func (s *ApplicationService) WithdrawAttendance(ctx context.Context, userID uuid.UUID) error { // Make atomic err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) - txEventRepo := s.eventsService.eventRepo.NewTx(tx) + txAppRepo := s.applicationRepo.NewTx(tx) + txUserRepo := s.userRepo.NewTx(tx) + if err := txAppRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ - UserID: userId, - EventID: eventId, + UserID: userID, StatusDoUpdate: true, Status: sqlc.ApplicationStatusWithdrawn, }); err != nil { return err } - return txEventRepo.UpdateRole(ctx, - userId, - eventId, - sqlc.EventRoleTypeApplicant, + return txUserRepo.UpdateRole(ctx, + sqlc.UpdateRoleParams{ + UserID: userID, + Role: sqlc.UserRoleApplicant, + }, ) }) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("WithdrawAttendance fail") return err } return nil } -func (s *ApplicationService) AcceptApplicationAcceptance(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { +func (s *ApplicationService) AcceptApplicationAcceptance(ctx context.Context, userID uuid.UUID) error { // is a check for a user being accepted necessary here? or is the frontend enough - err := s.eventsService.eventRepo.UpdateRole(ctx, - userId, - eventId, - sqlc.EventRoleTypeAttendee, + err := s.userRepo.UpdateRole(ctx, + sqlc.UpdateRoleParams{ + UserID: userID, + Role: sqlc.UserRoleAttendee, + }, ) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("AcceptApplicationAcceptance fail, unable to update role") return err } return nil } -func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Context, eventId uuid.UUID, acceptanceCount uint32, acceptanceQuota uint32) error { +func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Context, acceptanceCount uint32, acceptanceQuota uint32) error { var acceptedUserIds []uuid.UUID - event, err := s.eventsService.GetEventByID(ctx, eventId) + ErrEventAlreadyStarted := errors.New("the event has already started") + ErrFailedToGetContactEmail := errors.New("Failed to get contact email") + + hackathon, err := s.hackathonRepo.GetHackathon(ctx) if err != nil { - s.logger.Err(err).Msg(err.Error()) + s.logger.Err(err).Msg("TransitionWaitlistedApplications fail, unable to get hackathon") return err } currentTime := time.Now() - if currentTime.After(event.StartTime) { + if currentTime.After(hackathon.StartTime) { s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") return ErrEventAlreadyStarted } err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) + txAppRepo := s.applicationRepo.NewTx(tx) - err := txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) + err := txAppRepo.TransitionAcceptedApplicationsToWaitlist(ctx) if err != nil { s.logger.Err(err).Msg(err.Error()) return err } - attendeeCount, err := s.appRepo.GetAttendeeCountByEventId(ctx, eventId) + attendeeCount, err := s.applicationRepo.GetAttendeeCount(ctx) if err != nil { s.logger.Err(err).Msg("Failed to get total accepted application amount.") } @@ -633,7 +684,7 @@ func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Contex } s.logger.Info().Msgf("Acceptance count: %v", acceptanceCount) - acceptedUserIds, err = txAppRepo.TransitionWaitlistedApplicationsToAcceptedByEventID(ctx, eventId, acceptanceCount) + acceptedUserIds, err = txAppRepo.TransitionWaitlistedApplicationsToAccepted(ctx, int32(acceptanceCount)) if err != nil { s.logger.Err(err).Msg(err.Error()) return err @@ -648,8 +699,8 @@ func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Contex return err } - for _, userId := range acceptedUserIds { - userContactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userId) + for _, userID := range acceptedUserIds { + userContactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userID) if err != nil { s.logger.Err(err).Msg(err.Error()) return err @@ -669,3 +720,62 @@ func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Contex return nil } + +// Checks if application is opened +// +// Returns nil if yes, otherwise returns error +func (s *ApplicationService) isApplicationOpen(ctx context.Context) error { + hackathon, err := s.hackathonRepo.GetHackathon(ctx) + + if err != nil { + s.logger.Err(err).Msg("Submit application fail because can't retrieve hackathon") + return err + } + + now := time.Now() + isApplicationOpen := now.After(hackathon.ApplicationOpen) && now.Before(hackathon.ApplicationClose) + + if !isApplicationOpen { + return ErrApplicationNotOpened + } + + return nil +} + +func (s *ApplicationService) ReleaseDecisions(ctx context.Context, batRunId uuid.UUID) error { + batRun, err := s.batService.GetRunById(ctx, batRunId) + + if batRun.Status != sqlc.BatRunStatusCompleted { + return errors.New("This run status is not valid for this action.") + } + + if len(batRun.AcceptedApplicants) == 0 { + return errors.New("No applicants marked as accepted.") + } + + err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { + txAppRepo := s.applicationRepo.NewTx(tx) + + err := txAppRepo.UpdateApplicationsStatuses(ctx, sqlc.UpdateApplicationStatusParams{ + Status: sqlc.ApplicationStatusAccepted, + UserIds: batRun.AcceptedApplicants, + }) + if err != nil { + return err + } + return txAppRepo.UpdateApplicationsStatuses(ctx, sqlc.UpdateApplicationStatusParams{ + Status: sqlc.ApplicationStatusRejected, + UserIds: batRun.RejectedApplicants, + }) + }) + if err != nil { + return err + } + + err = s.emailService.SendDecisionEmails(ctx, batRun) + if err != nil { + return errors.New("Failed to send decision emails") + } + + return nil +} diff --git a/apps/api/internal/domains/auth/http.go b/apps/api/internal/domains/auth/http.go new file mode 100644 index 00000000..30085eef --- /dev/null +++ b/apps/api/internal/domains/auth/http.go @@ -0,0 +1,211 @@ +package auth + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "net" + "net/http" + "net/url" + "time" + + "github.com/danielgtaylor/huma/v2" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/config" +) + +func RegisterRoutes(authHandler *handler, group huma.API, mw *middleware.Middleware, config *config.Config) { + // if config.AppEnv == "dev" { + // huma.Register(group, huma.Operation{ + // OperationID: "login-with-discord", + // Method: http.MethodGet, + // Summary: "Login With Discord", + // Description: "Redirects to discord oauth to login", + // Tags: []string{"Auth"}, + // Path: "/login", + // Middlewares: huma.Middlewares{mw.Auth.RawHTTPMiddlewareHuma}, + // Errors: []int{http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadRequest, http.StatusUnauthorized}, + // }, authHandler.handleLogin) + // } + + huma.Register(group, huma.Operation{ + OperationID: "logout", + Method: http.MethodPost, + Summary: "Logout", + Description: "Logs out the authenticated user by invalidating their session", + Tags: []string{"Auth"}, + Path: "/logout", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + }, authHandler.handleLogout) + + huma.Register(group, huma.Operation{ + OperationID: "oauth-callback", + Method: http.MethodGet, + Summary: "OAuth Callback", + Description: "Handles the OAuth provider callback, validates state and nonce, and sets the session cookie.", + Tags: []string{"Auth"}, + Path: "/callback", + Middlewares: huma.Middlewares{mw.Auth.RawHTTPMiddlewareHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusNotImplemented, http.StatusBadRequest, http.StatusUnauthorized}, + }, authHandler.handleOAuthCallback) +} + +type handler struct { + authService *AuthService + config *config.Config + logger zerolog.Logger +} + +func NewHandler(authService *AuthService, config *config.Config, logger zerolog.Logger) *handler { + return &handler{ + authService: authService, + config: config, + logger: logger.With().Str("handler", "AuthHandler").Str("domain", "auth").Logger(), + } +} + +type LogoutOutput struct { + SetCookie http.Cookie `header:"Set-Cookie"` +} + +func (h *handler) handleLogout(ctx context.Context, input *struct{}) (*LogoutOutput, error) { + err := h.authService.Logout(ctx) + + if err != nil { + if errors.Is(err, ErrFetchSessionContextFailed) { + return nil, huma.Error401Unauthorized("Not authorized.") + } else { + return nil, huma.Error500InternalServerError("Something went wrong while logging out.") + } + } + + res := &LogoutOutput{ + SetCookie: http.Cookie{ + Name: h.config.Cookie.SessionName, + Value: "", + Domain: h.config.Cookie.Domain, + Path: "/", + HttpOnly: true, + Secure: h.config.Cookie.Secure, + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + }, + } + + return res, nil +} + +type OAuthState struct { + Nonce string `json:"nonce"` + Provider string `json:"provider"` + Redirect string `json:"redirect"` +} + +type OAuthCallbackOutput struct { + SetCookie []http.Cookie `header:"Set-Cookie"` + RedirectUrl string `header:"Location"` + Status int +} + +func (h *handler) handleOAuthCallback(ctx context.Context, input *struct { + Code string `query:"code" required:"true" doc:"OAuth authorization code"` + State string `query:"state" required:"true" doc:"Base64 encoded OAuth state"` + Nonce string `cookie:"sh_auth_nonce" required:"true" doc:"Auth nonce cookie for CSRF protection"` + UserAgent string `header:"User-Agent" doc:"Client user agent"` +}) (*OAuthCallbackOutput, error) { + r := ctx.Value(middleware.RawRequestKey{}).(*http.Request) + + var ipAddress *string + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err == nil && ip != "" { + ipAddress = &ip + } + + if input.Code == "" || input.State == "" { + return nil, huma.Error400BadRequest("Invalid callback. Please try again.") + } + + decodedStateBytes, err := base64.URLEncoding.DecodeString(input.State) + if err != nil { + return nil, huma.Error400BadRequest("This callback was invalid. Please try again.") + } + + var state OAuthState + if err := json.Unmarshal(decodedStateBytes, &state); err != nil { + return nil, huma.Error400BadRequest("This callback was invalid. Please try again.") + } + + // if h.config.AppEnv == "prod" { + // if input.Nonce != state.Nonce { + // return nil, huma.Error401Unauthorized("Failed to authenticate. Please try again.") + // } + // } + if input.Nonce != state.Nonce { + return nil, huma.Error401Unauthorized("Failed to authenticate. Please try again.") + } + + session, err := h.authService.AuthenticateWithOAuth(ctx, input.Code, state.Provider, ipAddress, &input.UserAgent) + if err != nil { + switch err { + case ErrProviderUnsupported: + return nil, huma.Error501NotImplemented("This provider is not supported.") + case ErrAuthenticationFailed: + return nil, huma.Error401Unauthorized("Failed to authenticate the user.") + default: + h.logger.Err(err).Msg("Something unexpected happened.") + return nil, huma.Error500InternalServerError("Something went wrong") + } + } + + if isURL(state.Redirect) { + return nil, huma.Error400BadRequest("invalid redirect path") + } + + redirectPath := ensureLeadingSlash(state.Redirect) + + res := &OAuthCallbackOutput{ + SetCookie: []http.Cookie{ + { + Name: h.config.Cookie.SessionName, + Value: session.ID.String(), + Domain: h.config.Cookie.Domain, + Path: "/", + HttpOnly: true, + Secure: h.config.Cookie.Secure, + SameSite: http.SameSiteLaxMode, + Expires: session.ExpiresAt, + }, + + { + Name: "sh_auth_nonce", + Value: "", + Domain: h.config.Cookie.Domain, + Path: "/", + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + }, + }, + + RedirectUrl: h.config.ClientUrl + redirectPath, + Status: http.StatusSeeOther, + } + + return res, nil +} + +func ensureLeadingSlash(s string) string { + if len(s) == 0 || s[0] != '/' { + return "/" + s + } + return s +} + +func isURL(s string) bool { + u, err := url.Parse(s) + return err == nil && u.Scheme != "" && u.Host != "" +} diff --git a/apps/api/internal/services/auth.go b/apps/api/internal/domains/auth/service.go similarity index 73% rename from apps/api/internal/services/auth.go rename to apps/api/internal/domains/auth/service.go index 2c0a901b..d8e7a258 100644 --- a/apps/api/internal/services/auth.go +++ b/apps/api/internal/domains/auth/service.go @@ -1,8 +1,7 @@ -package services +package auth import ( "context" - "database/sql" "errors" "fmt" "net/http" @@ -14,9 +13,9 @@ import ( "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/api/middleware" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" "github.com/swamphacks/core/apps/api/internal/oauth" ) @@ -32,25 +31,29 @@ type AuthService struct { userRepo *repository.UserRepository accountRepo *repository.AccountRepository sessionRepo *repository.SessionRepository - txm *db.TransactionManager - client *http.Client + txm *database.TransactionManager + httpClient *http.Client logger zerolog.Logger - authCfg *config.AuthConfig + authConfig *config.AuthConfig } -func NewAuthService(userRepo *repository.UserRepository, accountRepo *repository.AccountRepository, sessionRepo *repository.SessionRepository, txm *db.TransactionManager, client *http.Client, logger zerolog.Logger, authCfg *config.AuthConfig) *AuthService { +func NewService( + userRepo *repository.UserRepository, accountRepo *repository.AccountRepository, sessionRepo *repository.SessionRepository, + txm *database.TransactionManager, httpClient *http.Client, + logger zerolog.Logger, authConfig *config.AuthConfig, +) *AuthService { return &AuthService{ userRepo: userRepo, accountRepo: accountRepo, sessionRepo: sessionRepo, txm: txm, - client: client, + httpClient: httpClient, + authConfig: authConfig, logger: logger.With().Str("service", "AuthService").Str("component", "auth").Logger(), - authCfg: authCfg, } } -func (s *AuthService) AuthenticateWithOAuth(ctx context.Context, code, provider string, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { +func (s *AuthService) AuthenticateWithOAuth(ctx context.Context, code, provider string, ipAddress, userAgent *string) (*sqlc.Session, error) { switch provider { case "discord": return s.authenticateWithDiscord(ctx, code, ipAddress, userAgent) @@ -59,15 +62,6 @@ func (s *AuthService) AuthenticateWithOAuth(ctx context.Context, code, provider } } -func (s *AuthService) GetMe(ctx context.Context) (*middleware.UserContext, error) { - userContext, ok := ctx.Value(middleware.UserContextKey).(*middleware.UserContext) - if !ok || userContext == nil { - return nil, ErrFetchUserFailed - } - - return userContext, nil -} - func (s *AuthService) Logout(ctx context.Context) error { sessionContext, ok := ctx.Value(middleware.SessionContextKey).(*middleware.SessionContext) if !ok || sessionContext == nil { @@ -82,15 +76,15 @@ func (s *AuthService) Logout(ctx context.Context) error { return nil } -func (s *AuthService) authenticateWithDiscord(ctx context.Context, code string, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { - discordOAuthResp, err := oauth.ExchangeDiscordCode(ctx, s.client, &s.authCfg.Discord, code) +func (s *AuthService) authenticateWithDiscord(ctx context.Context, code string, ipAddress, userAgent *string) (*sqlc.Session, error) { + discordOAuthResp, err := oauth.ExchangeDiscordCode(ctx, s.httpClient, &s.authConfig.Discord, code) if err != nil { // Log it s.logger.Err(err).Msg("Failed to exchange discord code for user authentication") return nil, ErrAuthenticationFailed } - discordUser, err := oauth.GetDiscordUserInfo(ctx, s.client, discordOAuthResp.AccessToken) + discordUser, err := oauth.GetDiscordUserInfo(ctx, s.httpClient, discordOAuthResp.AccessToken) if err != nil { return nil, fmt.Errorf("%w: provider=discord", ErrFetchUserFailed) } @@ -101,7 +95,7 @@ func (s *AuthService) authenticateWithDiscord(ctx context.Context, code string, AccountID: discordUser.ID, }) - if err != nil && errors.Is(err, sql.ErrNoRows) { + if err != nil && errors.Is(err, database.ErrAccountNotFound) { return s.registerNewDiscordUser(ctx, discordUser, discordOAuthResp, ipAddress, userAgent) } else if err != nil { return nil, err @@ -110,8 +104,8 @@ func (s *AuthService) authenticateWithDiscord(ctx context.Context, code string, return s.createSessionForExistingUser(ctx, account.UserID, ipAddress, userAgent) } -func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oauth.DiscordUserWithAvatarURL, oauthResp *oauth.DiscordExchangeResponse, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { - var session *sqlc.AuthSession +func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oauth.DiscordUserWithAvatarURL, oauthResp *oauth.DiscordExchangeResponse, ipAddress, userAgent *string) (*sqlc.Session, error) { + var session *sqlc.Session err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { txUserRepo := s.userRepo.NewTx(tx) @@ -125,7 +119,7 @@ func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oaut avatar = &custom } - user, err := txUserRepo.Create(ctx, sqlc.CreateUserParams{ + user, err := txUserRepo.CreateUser(ctx, sqlc.CreateUserParams{ Name: userInfo.Username, Email: &userInfo.Email, Image: avatar, @@ -167,7 +161,7 @@ func (s *AuthService) registerNewDiscordUser(ctx context.Context, userInfo *oaut return session, nil } -func (s *AuthService) createSessionForExistingUser(ctx context.Context, userID uuid.UUID, ipAddress, userAgent *string) (*sqlc.AuthSession, error) { +func (s *AuthService) createSessionForExistingUser(ctx context.Context, userID uuid.UUID, ipAddress, userAgent *string) (*sqlc.Session, error) { return s.sessionRepo.Create(ctx, sqlc.CreateSessionParams{ UserID: userID, ExpiresAt: time.Now().AddDate(0, 1, 0), @@ -176,7 +170,6 @@ func (s *AuthService) createSessionForExistingUser(ctx context.Context, userID u }) } -/* HELPER FUNCTIONS BELOW THIS LINE */ func expiresAt(duration time.Duration) *time.Time { expiredAtTime := time.Now().Add(duration) return &expiredAtTime diff --git a/apps/api/internal/bat/engine.go b/apps/api/internal/domains/bat/engine.go similarity index 100% rename from apps/api/internal/bat/engine.go rename to apps/api/internal/domains/bat/engine.go diff --git a/apps/api/internal/domains/bat/http.go b/apps/api/internal/domains/bat/http.go new file mode 100644 index 00000000..aedd5394 --- /dev/null +++ b/apps/api/internal/domains/bat/http.go @@ -0,0 +1,134 @@ +package bat + +import ( + "context" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func RegisterRoutes(batHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-bat-runs", + Method: http.MethodGet, + Summary: "Get Bat Runs", + Description: "Returns all bat runs", + Tags: []string{"Bat"}, + Path: "/runs", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, + }, batHandler.handleGetRuns) + + huma.Register(group, huma.Operation{ + OperationID: "delete-bat-run", + Method: http.MethodDelete, + Summary: "Delete Bat Run", + Description: "Delete a bat run by id", + Tags: []string{"Bat"}, + Path: "/runs/{runId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, + }, batHandler.handleDeleteRun) + + huma.Register(group, huma.Operation{ + OperationID: "queue-schedule-waitlist-transition-task", + Method: http.MethodPost, + Summary: "Queue Waitlist Transition Task", + Description: "Queues an asynq task that transitions waitlisted applications, running every 3 days.", + Tags: []string{"Bat"}, + Path: "/begin-waitlist-transition", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, + }, batHandler.handleQueueScheduleWaitlistTransitionTask) + + huma.Register(group, huma.Operation{ + OperationID: "queue-shutdown-waitlist-scheduler-task", + Method: http.MethodPost, + Summary: "Queue Shutdown Waitlist Scheduler", + Description: "Shutsdown the scheduler used for the waitlist transition task. Error returned through logs if a scheduler is not active.", + Tags: []string{"Bat"}, + Path: "/shutdown-waitlist-scheduler", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, + }, batHandler.handleQueueShutdownWaitlistSchedulerTask) +} + +type handler struct { + BatService *BatService + logger zerolog.Logger +} + +func NewHandler(BatService *BatService, logger zerolog.Logger) *handler { + return &handler{ + BatService: BatService, + logger: logger.With().Str("handler", "BatHandler").Str("domain", "bat").Logger(), + } +} + +type GetRunsOutput struct { + Body *[]sqlc.BatRun +} + +func (h *handler) handleGetRuns(ctx context.Context, input *struct{}) (*GetRunsOutput, error) { + runs, err := h.BatService.GetRuns(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to get bat runs") + } + + return &GetRunsOutput{Body: runs}, nil +} + +type DeleteRunOutput struct { + Status int +} + +func (h *handler) handleDeleteRun(ctx context.Context, input *struct { + RunId string `path:"runId"` +}) (*DeleteRunOutput, error) { + runId, err := uuid.Parse(input.RunId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid run id") + } + + err = h.BatService.DeleteRunById(ctx, runId) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to delete run by id") + } + + return &DeleteRunOutput{Status: http.StatusOK}, nil +} + +type QueueScheduleWaitlistTransitionTaskOutput struct { + Status int +} + +func (h *handler) handleQueueScheduleWaitlistTransitionTask(ctx context.Context, input *struct{}) (*QueueScheduleWaitlistTransitionTaskOutput, error) { + err := h.BatService.QueueScheduleWaitlistTransitionTask(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to queue schedule waitlist transition task") + } + + return &QueueScheduleWaitlistTransitionTaskOutput{Status: http.StatusOK}, nil +} + +type QueueShutdownWaitlistSchedulerTaskOutput struct { + Status int +} + +func (h *handler) handleQueueShutdownWaitlistSchedulerTask(ctx context.Context, input *struct{}) (*QueueShutdownWaitlistSchedulerTaskOutput, error) { + err := h.BatService.QueueShutdownWaitlistScheduler() + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to queue shutdown waitlist scheduler task") + } + + return &QueueShutdownWaitlistSchedulerTaskOutput{Status: http.StatusOK}, nil +} diff --git a/apps/api/internal/domains/bat/service.go b/apps/api/internal/domains/bat/service.go new file mode 100644 index 00000000..935457f5 --- /dev/null +++ b/apps/api/internal/domains/bat/service.go @@ -0,0 +1,266 @@ +package bat + +import ( + "context" + "encoding/json" + "errors" + + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/domains/email" + "github.com/swamphacks/core/apps/api/internal/tasks" +) + +type BatService struct { + applicationRepo *repository.ApplicationRepository + hackathonRepo *repository.HackathonRepository + userRepo *repository.UserRepository + batRunsRepo *repository.BatRunsRepository + emailService *email.EmailService + txm *database.TransactionManager + taskQueue *asynq.Client + scheduler *asynq.Scheduler + config *config.Config + logger zerolog.Logger +} + +func NewBatService( + applicationRepo *repository.ApplicationRepository, hackathonRepo *repository.HackathonRepository, userRepo *repository.UserRepository, + batRunsRepo *repository.BatRunsRepository, emailService *email.EmailService, txm *database.TransactionManager, + taskQueue *asynq.Client, scheduler *asynq.Scheduler, config *config.Config, logger zerolog.Logger) *BatService { + return &BatService{ + taskQueue: taskQueue, + scheduler: scheduler, + applicationRepo: applicationRepo, + hackathonRepo: hackathonRepo, + userRepo: userRepo, + batRunsRepo: batRunsRepo, + emailService: emailService, + txm: txm, + config: config, + logger: logger.With().Str("service", "Bat Service").Logger(), + } +} + +var ( + ErrRunConflict = errors.New("Run already exists for this event") + ErrFailedToAddRun = errors.New("Failed to add run") +) + +func (s *BatService) AddRun(ctx context.Context) (*sqlc.BatRun, error) { + + // TODO: don't hardcode the hackathonId + run, err := s.batRunsRepo.AddRun(ctx, "xii") + if err != nil && errors.Is(err, database.ErrDuplicateRun) { + s.logger.Err(err).Msg("Could not insert result as it already exists.") + return nil, ErrRunConflict + } else if err != nil { + s.logger.Err(err).Msg("An unknown error was caught!") + return nil, ErrFailedToAddRun + } + + return run, nil +} + +func (s *BatService) GetRuns(ctx context.Context) (*[]sqlc.BatRun, error) { + return s.batRunsRepo.GetRuns(ctx) +} + +func (s *BatService) GetRunById(ctx context.Context, batId uuid.UUID) (sqlc.BatRun, error) { + return s.batRunsRepo.GetRunById(ctx, batId) +} + +func (s *BatService) UpdateRunById(ctx context.Context, params sqlc.UpdateBatRunByIdParams) (*sqlc.BatRun, error) { + err := s.batRunsRepo.UpdateRunById(ctx, params) + if err != nil { + s.logger.Err(err).Msg("update run by id failed") + return nil, err + } + + run, err := s.batRunsRepo.GetRunById(ctx, params.ID) + + return &run, err +} + +func (s *BatService) DeleteRunById(ctx context.Context, id uuid.UUID) error { + err := s.batRunsRepo.DeleteRunById(ctx, id) + if err != nil { + s.logger.Err(err).Msg("delete run by id failed") + return errors.New("Failed to delete run") + } + + return err +} + +func (s *BatService) QueueCalculateAdmissionsTask(ctx context.Context) (*asynq.TaskInfo, error) { + newRun, err := s.AddRun(ctx) + if err != nil { + return nil, ErrFailedToAddRun + } + + task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ + BatRunID: newRun.ID, + }) + if err != nil { + s.logger.Err(err).Msg("Failed to create CalculateAdmissions task") + return nil, err + } + + info, err := s.taskQueue.Enqueue(task, asynq.Queue("bat")) + if err != nil { + s.logger.Err(err).Msg("Failed to queue CalculateAdmissions task") + return nil, err + } + + return info, nil +} + +func (s *BatService) CalculateAdmissions(ctx context.Context, batRunId uuid.UUID) error { + s.logger.Debug().Str("RunID", batRunId.String()).Msg("") + + // check to make sure reviews are done, update state if true, return error if not + nonReviewedApplicantUUIDs, err := s.applicationRepo.GetNonReviewedApplications(ctx) + if err != nil { + return errors.New("Could not get determine if application reviews have finished.") + } + + areReviewsComplete := len(nonReviewedApplicantUUIDs) == 0 + + if !areReviewsComplete { + return errors.New("Please make sure reviews are finished before calculating application decisions.") + } + + engine, err := NewBatEngine(0.6, 0.4) + if err != nil { + return err + } + + // Aggregate data necessary + applications, err := s.applicationRepo.ListAdmissionCandidates(ctx) + if err != nil || len(applications) == 0 { + return errors.New("Failed to retrieve applications") + } + + admissionCandidates, err := s.mapToCandidates(engine, applications) + if err != nil { + return err + } + + teams, idvs := engine.GroupCandidates(admissionCandidates) + acceptedTeamMembers, remainder := engine.AcceptTeams(teams) + + idvs = append(idvs, remainder...) + acceptedIdvs, rejected := engine.AcceptIndividuals(idvs) + + accepted := append(acceptedTeamMembers, acceptedIdvs...) + + acceptedIDs := make([]uuid.UUID, 0, len(accepted)) + rejectedIDs := make([]uuid.UUID, 0, len(rejected)) + + for _, applicant := range accepted { + acceptedIDs = append(acceptedIDs, applicant.UserID) + } + for _, applicant := range rejected { + rejectedIDs = append(rejectedIDs, applicant.UserID) + } + + params := sqlc.UpdateBatRunByIdParams{ + // TODO: add UF/other/early/late info? + AcceptedApplicantsDoUpdate: true, + RejectedApplicantsDoUpdate: true, + StatusDoUpdate: true, + AcceptedApplicants: acceptedIDs, + RejectedApplicants: rejectedIDs, + Status: sqlc.BatRunStatusCompleted, + ID: batRunId, + } + + err = s.batRunsRepo.UpdateRunById(ctx, params) + if err != nil { + return errors.New("Failed to update run") + } + + s.logger.Info().Int("Teams Members Accepted", int(len(acceptedTeamMembers))).Int("Accepted", int(engine.Quota.TotalAccepted)).Int("Rejected", len(rejected)).Msg("Finished Algo") + + return nil +} + +// This could be moved into the engine instead, or some mapping function within the bat package. +func (s *BatService) mapToCandidates(engine *BatEngine, applications []sqlc.ListAdmissionCandidatesRow) ([]AdmissionCandidate, error) { + var appAdmissionsData []AdmissionCandidate + for _, app := range applications { + if app.ExperienceRating == nil || app.PassionRating == nil { + return []AdmissionCandidate{}, errors.New("Some applications are missing their review ratings") + } + + var admissionContext AdmissionContext + if err := json.Unmarshal(app.Application, &admissionContext); err != nil { + s.logger.Debug().Bytes("App", app.Application).Msg("Application data") + return []AdmissionCandidate{}, err + } + + var teamId uuid.UUID + if app.TeamID != nil { + teamId = *app.TeamID + } + + wScore, err := engine.CalculateWeightedScore(*app.PassionRating, *app.ExperienceRating) + if err != nil { + return []AdmissionCandidate{}, err + } + appAdmissionsData = append(appAdmissionsData, AdmissionCandidate{ + UserID: app.UserID, + TeamID: uuid.NullUUID{ + UUID: teamId, + Valid: app.TeamID != nil, + }, + WeightedScore: wScore, + SortKey: 0.0, + IsUFStudent: admissionContext.School == "University of Florida", + IsEarlyCareer: admissionContext.Year == "first_year" || admissionContext.Year == "second_year", + }) + } + + return appAdmissionsData, nil +} + +func (s *BatService) QueueScheduleWaitlistTransitionTask(ctx context.Context) error { + task, err := tasks.NewTaskScheduleTransitionWaitlist(tasks.ScheduleTransitionWaitlistPayload{ + Period: s.config.AcceptFromWaitlistPeriod, + }) + if err != nil { + s.logger.Err(err).Msg("Failed to create ScheduleTransitionWaitlist task") + return err + } + + _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) + if err != nil { + s.logger.Err(err).Msg("Failed to queue ScheduleTransitionWaitlist task") + return err + } + s.logger.Info().Msg("Queued TransitionWaitlist task") + + return nil +} + +func (s *BatService) QueueShutdownWaitlistScheduler() error { + task, err := tasks.NewTaskShutdownScheduler() + if err != nil { + s.logger.Err(err).Msg("Failed to create ShutdownWaitlistScheduler task") + return err + } + + _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) + if err != nil { + s.logger.Err(err).Msg("Failed to queue ShutdownWaitlistScheduler task") + return err + } + s.logger.Info().Msg("Queued ShutdownWaitlistScheduler task") + + return nil +} diff --git a/apps/api/internal/domains/email/http.go b/apps/api/internal/domains/email/http.go new file mode 100644 index 00000000..43851baf --- /dev/null +++ b/apps/api/internal/domains/email/http.go @@ -0,0 +1,164 @@ +package email + +import ( + "context" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/emailutils" +) + +func RegisterRoutes(emailHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "queue-text-email", + Method: http.MethodPost, + Summary: "Queue Text Email", + Description: "Pushes a text email request to the task queue", + Tags: []string{"Email"}, + Path: "/queue-text-email", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, + }, emailHandler.handleQueueTextEmail) + + huma.Register(group, huma.Operation{ + OperationID: "queue-confirmation-email", + Method: http.MethodPost, + Summary: "Queue Confirmation Email", + Description: "Pushes a confirmation email request to the task queue", + Tags: []string{"Email"}, + Path: "/queue-confirmation-email", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, + }, emailHandler.handleQueueConfirmationEmail) + + huma.Register(group, huma.Operation{ + OperationID: "queue-welcome-email", + Method: http.MethodPost, + Summary: "Queue Welcome Email", + Description: "Pushes a welcome email request to the task queue", + Tags: []string{"Email"}, + Path: "/queue-welcome-email", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusUnauthorized}, + }, emailHandler.handleQueueWelcomeEmail) + + huma.Register(group, huma.Operation{ + OperationID: "send-welcome-emails", + Method: http.MethodPost, + Summary: "Send Welcome Emails", + Description: "Send welcome emails to all attendees", + Tags: []string{"Email"}, + Path: "/send-welcome-emails", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusInternalServerError, http.StatusUnauthorized}, + }, emailHandler.handleSendWelcomeEmails) +} + +type handler struct { + emailService *EmailService + logger zerolog.Logger +} + +func NewHandler(emailService *EmailService, logger zerolog.Logger) *handler { + return &handler{ + emailService: emailService, + logger: logger.With().Str("handler", "EmailHandler").Str("domain", "email").Logger(), + } +} + +type QueueTextEmailRequest struct { + To []string `json:"to"` + Subject string `json:"subject" minLength:"1"` + Body string `json:"body" minLength:"1"` +} + +type QueueTextEmailOutput struct { + Status int +} + +func (h *handler) handleQueueTextEmail(ctx context.Context, input *struct { + Body QueueTextEmailRequest +}) (*QueueTextEmailOutput, error) { + for _, to := range input.Body.To { + if !emailutils.IsValidEmail(to) { + return nil, huma.Error400BadRequest("Invalid email(s)") + } + } + + taskInfo, err := h.emailService.QueueSendTextEmail(input.Body.To, input.Body.Subject, input.Body.Body) + + if err != nil { + h.logger.Err(err).Msg("Failed to queue SendTextEmail from EmailHandler") + return nil, huma.Error500InternalServerError("Failed to queue text email") + } + + h.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued SendTextEmail task!") + + return &QueueTextEmailOutput{Status: http.StatusOK}, nil +} + +type QueueConfirmationEmailRequest struct { + Email string `json:"email" required:"true"` + FirstName string `json:"firstName" required:"true"` +} + +type QueueConfirmationEmailOutput struct { + Status int +} + +func (h *handler) handleQueueConfirmationEmail(ctx context.Context, input *struct { + Body QueueConfirmationEmailRequest +}) (*QueueConfirmationEmailOutput, error) { + err := h.emailService.QueueConfirmationEmail(input.Body.Email, input.Body.FirstName) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to queue confirmation email") + } + + return &QueueConfirmationEmailOutput{Status: http.StatusOK}, nil +} + +type QueueWelcomeEmailRequest struct { + Email string `json:"email" required:"true"` + FirstName string `json:"firstName" required:"true"` + RecipientId string `json:"recipientId" required:"true"` +} + +type QueueWelcomeEmailOutput struct { + Status int +} + +func (h *handler) handleQueueWelcomeEmail(ctx context.Context, input *struct { + Body QueueWelcomeEmailRequest +}) (*QueueWelcomeEmailOutput, error) { + recipientId, err := uuid.Parse(input.Body.RecipientId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid recipient id") + } + + err = h.emailService.QueueWelcomeEmail(ctx, input.Body.Email, input.Body.FirstName, recipientId) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to queue welcome email") + } + + return &QueueWelcomeEmailOutput{Status: http.StatusOK}, nil +} + +type SendWelcomeEmailsOutput struct { + Status int +} + +func (h *handler) handleSendWelcomeEmails(ctx context.Context, input *struct{}) (*SendWelcomeEmailsOutput, error) { + err := h.emailService.SendWelcomeEmailToAttendees(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to send welcome emails") + } + + return &SendWelcomeEmailsOutput{Status: http.StatusOK}, nil +} diff --git a/apps/api/internal/domains/email/service.go b/apps/api/internal/domains/email/service.go new file mode 100644 index 00000000..99e3ec22 --- /dev/null +++ b/apps/api/internal/domains/email/service.go @@ -0,0 +1,296 @@ +package email + +import ( + "bytes" + "context" + "errors" + "fmt" + "text/template" + + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/rs/zerolog" + "github.com/skip2/go-qrcode" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/emailutils" + "github.com/swamphacks/core/apps/api/internal/storage" + "github.com/swamphacks/core/apps/api/internal/tasks" +) + +type EmailService struct { + hackathonRepo *repository.HackathonRepository + userRepo *repository.UserRepository + logger zerolog.Logger + taskQueue *asynq.Client + SESClient *emailutils.SESClient + storage storage.Storage + config *config.Config +} + +func NewEmailService( + hackathonRepo *repository.HackathonRepository, userRepo *repository.UserRepository, + taskQueue *asynq.Client, SESClient *emailutils.SESClient, storage storage.Storage, + logger zerolog.Logger, config *config.Config, +) *EmailService { + return &EmailService{ + hackathonRepo: hackathonRepo, + userRepo: userRepo, + logger: logger.With().Str("service", "EmailService").Str("component", "email").Logger(), + taskQueue: taskQueue, + SESClient: SESClient, + storage: storage, + config: config, + } +} + +func (s *EmailService) QueueConfirmationEmail(recipient string, name string) error { + subject := "SwampHacks XII: we received your application!" + templateEmailFilepath := s.config.EmailTemplateDirectory + "ConfirmationEmail.html" + + type emailTemplateData struct { + Name string + } + _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) + + if err != nil { + s.logger.Err(err).Msg("Failed to send confirmation email to recipient") + return err + } + + return nil +} + +func (s *EmailService) QueueWelcomeEmail(ctx context.Context, recipient string, name string, userID uuid.UUID) error { + qrString := fmt.Sprintf("IDENT::%s", userID) + qrPng, err := qrcode.Encode(qrString, qrcode.Medium, 256) + if err != nil { + s.logger.Err(err).Msg("Failed to generate QR code png") + return err + } + + contentType := "image/png" + if s.storage == nil { + s.logger.Err(err).Msg("A R2 client must be connected for this function to run") + return err + } + err = s.storage.Store(ctx, s.config.CoreBuckets.QRCodes, userID.String(), qrPng, &contentType) + if err != nil { + s.logger.Err(err).Msg("Failed to upload QR code to R2") + return err + } + + qrPngLink := fmt.Sprintf("%s/%s", s.config.CoreBuckets.QRCodesBaseUrl, userID.String()) + + subject := "SwampHacks XII – A welcome from our Organizers!" + templateEmailFilepath := s.config.EmailTemplateDirectory + "WelcomeEmail.html" + + type emailTemplateData struct { + Name string + QRPngLink string + } + _, err = s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name, QRPngLink: qrPngLink}, templateEmailFilepath) + + if err != nil { + s.logger.Err(err).Msgf("Failed to send welcome email to recipient with userID %s", userID.String()) + return err + } + + return nil +} + +func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name string) error { + subject := "Congratulations! You're in – confirm in 72 hours to keep your spot in SwampHacks XII" + templateEmailFilepath := s.config.EmailTemplateDirectory + "WaitlistAcceptanceEmail.html" + + type emailTemplateData struct { + Name string + } + _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) + + if err != nil { + s.logger.Err(err).Msg("Failed to send waitlist acceptance email to recipient") + return err + } + + return nil +} + +func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, templateData interface{}, templateFilePath string) (*asynq.TaskInfo, error) { + if len(to) == 0 { + s.logger.Warn().Msgf("No recipient email found for email being sent from template '%s'", templateFilePath) + } + + task, err := tasks.NewTaskSendHtmlEmail(tasks.SendHtmlEmailPayload{ + To: to, + Subject: subject, + TemplateData: templateData, + TemplateFilePath: templateFilePath, + }) + + if err != nil { + s.logger.Err(err).Msg("Failed to create SendHtmlEmail task") + return nil, err + } + + taskInfo, err := s.taskQueue.Enqueue(task, asynq.Queue("email")) + s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued SendHtmlEmail task!") + if err != nil { + s.logger.Err(err).Msg("Failed to queue SendHtmlEmail task") + return nil, err + } + + return taskInfo, nil +} + +func (s *EmailService) QueueSendTextEmail(to []string, subject string, body string) (*asynq.TaskInfo, error) { + task, err := tasks.NewTaskSendTextEmail(tasks.SendTextEmailPayload{ + To: to, + Subject: subject, + Body: body, + }) + + if err != nil { + s.logger.Err(err).Msg("Failed to create SendTextEmail task") + return nil, err + } + + info, err := s.taskQueue.Enqueue(task, asynq.Queue("email")) + if err != nil { + s.logger.Err(err).Msg("Failed to queue SendTextEmail task") + return nil, err + } + + return info, nil +} + +// SendHtmlEmail +// +// templateData: a struct holding the data which should replace {{}} tags inside of an html template. +// For example, if an email template uses the tag {{ .Name }}, then the templateData struct would look like +// type templateData struct { +// Name string +// } +func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateData interface{}, templateFilePath string) error { + var body bytes.Buffer + + template, err := template.ParseFiles(templateFilePath) + if err != nil { + s.logger.Err(err).Msg("Failed to parse email template for recipient") + } + + err = template.Execute(&body, templateData) + if err != nil { + s.logger.Err(err).Msg("Failed to inject template variables for recipient '%s'.") + } + + err = s.SESClient.SendHTMLEmail([]string{recipient}, "noreply@swamphacks.com", subject, body.String()) + if err != nil { + s.logger.Err(err).Msg("Failed to send html email to recipient") + return err + } + s.logger.Info().Str("Template", templateFilePath).Msg("Sent email") + + return nil +} + +func (s *EmailService) SendWelcomeEmailToAttendees(ctx context.Context) error { + attendees, err := s.hackathonRepo.GetAttendeeUserIds(ctx) + if err != nil { + s.logger.Err(err).Msg("Could not get attendee user ids") + return err + } + + s.logger.Info().Msgf("Sending welcome emails to %v attendees", len(attendees)) + + for _, userID := range attendees { + contactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userID) + if err != nil { + s.logger.Err(err).Msgf("Could not get contact info for user with id %s", userID) + return err + } + contactEmail, ok := contactInfo.ContactEmail.(string) + if !ok { + s.logger.Err(err).Msgf("could got convert id %s", userID) + continue + } + if contactEmail == "" { + s.logger.Err(err).Msgf("empty contact email found for user with id %s", userID) + continue + } + + err = s.QueueWelcomeEmail(ctx, contactEmail, contactInfo.Name, userID) + if err != nil { + s.logger.Err(err).Msgf("Could not queue welcome email for user with id %s", userID) + return err + } + } + return nil +} + +var ( + ErrListApplicationsFailure = errors.New("Failed to retrieve applications") + ErrMissingRatings = errors.New("Some applications are missing their review ratings") + ErrRunConflict = errors.New("Run already exists for this event") + ErrFailedToAddRun = errors.New("Failed to add run") + ErrFailedToDeleteRun = errors.New("Failed to delete run") + ErrFailedToUpdateRun = errors.New("Failed to update run") + ErrCouldNotGetEventInfo = errors.New("Could not retreive event info.") + ErrReviewsNotFinished = errors.New("Please make sure reviews are finished before calculating application decisions.") + ErrRunMismatch = errors.New("That bat run does not belong to this event.") + ErrRunStatusInvalid = errors.New("This run status is not valid for this action.") + ErrNoAcceptedApplicants = errors.New("No applicants marked as accepted.") + ErrFailedToCheckAppReviewsComplete = errors.New("Could not get determine if application reviews have finished.") + ErrReviewsNotComplete = errors.New("Please make sure reviews are finished before calculating application decisions.") + ErrCouldNotGetEmailInfo = errors.New("Could not get email info for applicant.") + ErrParseTemplateFilepathFailed = errors.New("Could not parse filepath for template.") + ErrFailedToSendDecisionEmails = errors.New("Failed to send decision emails") + ErrTestErr = errors.New("Err while testing") + ErrFailedToGetContactEmail = errors.New("Failed to get contact email") + ErrUserNotAttendee = errors.New("user is not an attendee") + ErrUserCheckedIn = errors.New("user already checked in") +) + +func (s *EmailService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun) error { + accepetedEmailTemplatePath := s.config.EmailTemplateDirectory + "ApplicationAcceptedEmail.html" + rejectedEmailTemplatePath := s.config.EmailTemplateDirectory + "ApplicationRejectedEmail.html" + acceptedEmailSubject := "Congratulations on being accepted to hack in SwampHacks XII!" + rejectedEmailSubject := "Update on Your SwampHacks XII Application" + + for _, uuid := range batRun.AcceptedApplicants { + emailInfo, err := s.userRepo.GetUserEmailInfoById(ctx, uuid) + if err != nil { + return ErrCouldNotGetEmailInfo + } + + contactEmail, ok := emailInfo.ContactEmail.(string) + if !ok { + return ErrFailedToGetContactEmail + } + type emailTemplateData struct { + Name string + } + taskInfo, err := s.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailTemplateData{Name: emailInfo.Name}, accepetedEmailTemplatePath) + s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued acceptance email") + } + + for _, uuid := range batRun.RejectedApplicants { + emailInfo, err := s.userRepo.GetUserEmailInfoById(ctx, uuid) + if err != nil { + return ErrCouldNotGetEmailInfo + } + + contactEmail, ok := emailInfo.ContactEmail.(string) + if !ok { + return ErrFailedToGetContactEmail + } + type emailTemplateData struct { + Name string + } + taskInfo, err := s.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailTemplateData{emailInfo.Name}, rejectedEmailTemplatePath) + s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued rejection email") + } + + return nil +} diff --git a/apps/api/internal/domains/hackathon/http.go b/apps/api/internal/domains/hackathon/http.go new file mode 100644 index 00000000..03c811d7 --- /dev/null +++ b/apps/api/internal/domains/hackathon/http.go @@ -0,0 +1,456 @@ +package hackathon + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/emailutils" + . "github.com/swamphacks/core/apps/api/internal/parse" +) + +func RegisterRoutes(hackathonHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon", + Method: http.MethodGet, + Summary: "Get Hackathon", + Description: "Returns public information of the hackathon", + Tags: []string{"Hackathon"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetHackathon) + + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon-for-staff", + Method: http.MethodGet, + Summary: "Get Detailed Hackathon", + Description: "Returns all information of the hackathon", + Tags: []string{"Hackathon"}, + Path: "/detailed", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetHackathonForStaff) + + huma.Register(group, huma.Operation{ + OperationID: "update-hackathon", + Method: http.MethodPatch, + Summary: "Update Hackathon", + Description: "Updates the information of the hackathon", + Tags: []string{"Hackathon"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, hackathonHandler.handleUpdateHackathon) + + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon-staff", + Method: http.MethodGet, + Summary: "Get Hackathon Staff", + Description: "Returns the users who are part of the current staff of the hackathon", + Tags: []string{"Hackathon"}, + Path: "/staff", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetStaff) + + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon-attendees-with-discord", + Method: http.MethodGet, + Summary: "Get Hackathon Attendees with Discord", + Description: "Returns all users with a discord account that is also attending the hackathon", + Tags: []string{"Hackathon"}, + Path: "/attendees/discord", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetAttendeesWithDiscord) + + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon-attendees-userids", + Method: http.MethodGet, + Summary: "Get Hackathon Attendees User Ids", + Description: "Returns all users ids of users who are attending the hackathon", + Tags: []string{"Hackathon"}, + Path: "/attendees/userids", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetAttendeeUserIds) + + huma.Register(group, huma.Operation{ + OperationID: "get-hackathon-attendees-count", + Method: http.MethodGet, + Summary: "Get Hackathon Attendees Count", + Description: "Returns the number of users who is attending the hackathon", + Tags: []string{"Hackathon"}, + Path: "/attendees/count", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, hackathonHandler.handleGetAttendeeCount) + + huma.Register(group, huma.Operation{ + OperationID: "check-in", + Method: http.MethodPost, + Summary: "Check In User", + Description: "Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.", + Tags: []string{"Hackathon"}, + Path: "/checkin", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, hackathonHandler.handleCheckIn) + + huma.Register(group, huma.Operation{ + OperationID: "submit-interest-email", + Method: http.MethodPost, + Summary: "Submit Interest Email", + Description: "Submits an email to interest/mailing list for the hackathon", + Tags: []string{"Hackathon"}, + Path: "/interest", // public route + Errors: []int{http.StatusBadRequest, http.StatusInternalServerError}, + DefaultStatus: http.StatusOK, + }, hackathonHandler.handleSubmitInterestEmail) + + huma.Register(group, huma.Operation{ + OperationID: "upload-banner", + Method: http.MethodPost, + Summary: "Upload Banner", + Description: "Uploads an image to be used as the banner for the hackathon", + Tags: []string{"Hackathon"}, + Path: "/banner", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + Errors: []int{http.StatusBadRequest, http.StatusInternalServerError}, + }, hackathonHandler.handleUploadBanner) + + huma.Register(group, huma.Operation{ + OperationID: "delete-banner", + Method: http.MethodDelete, + Summary: "Delete Banner", + Description: "Deletes the banner", + Tags: []string{"Hackathon"}, + Path: "/banner", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + Errors: []int{http.StatusBadRequest, http.StatusInternalServerError}, + }, hackathonHandler.handleDeleteBanner) +} + +type handler struct { + hackathonService *HackathonService + config *config.Config + logger zerolog.Logger +} + +func NewHandler(hackathonService *HackathonService, config *config.Config, logger zerolog.Logger) *handler { + return &handler{ + hackathonService: hackathonService, + config: config, + logger: logger.With().Str("handler", "HackathonHandler").Str("domain", "hackathon").Logger(), + } +} + +type PublicHackathon struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"locationUrl"` + ApplicationOpen time.Time `json:"applicationOpen"` + ApplicationClose time.Time `json:"applicationClose"` + RsvpDeadline *time.Time `json:"rsvpDeadline"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Banner *string `json:"banner"` +} + +type GetHackathonOutput struct { + Body PublicHackathon +} + +func (h *handler) handleGetHackathon(ctx context.Context, input *struct{}) (*GetHackathonOutput, error) { + hackathon, err := h.hackathonService.GetHackathon(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + if errors.Is(err, database.ErrEntityNotFound) { + return nil, huma.Error404NotFound("Hackathon not found") + } + return nil, huma.Error500InternalServerError("Failed to get hackathon") + } + + return &GetHackathonOutput{Body: PublicHackathon{ + ID: hackathon.ID, + Name: hackathon.Name, + Description: hackathon.Description, + Location: hackathon.Location, + LocationUrl: hackathon.LocationUrl, + ApplicationOpen: hackathon.ApplicationOpen, + ApplicationClose: hackathon.ApplicationClose, + RsvpDeadline: hackathon.RsvpDeadline, + StartTime: hackathon.StartTime, + EndTime: hackathon.EndTime, + Banner: hackathon.Banner, + }}, nil +} + +type GetHackathonForStaffOutput struct { + Body *sqlc.Hackathon +} + +func (h *handler) handleGetHackathonForStaff(ctx context.Context, input *struct{}) (*GetHackathonForStaffOutput, error) { + hackathon, err := h.hackathonService.GetHackathon(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + if errors.Is(err, database.ErrEntityNotFound) { + return nil, huma.Error404NotFound("Hackathon not found") + } + return nil, huma.Error500InternalServerError("Failed to get hackathon") + } + + return &GetHackathonForStaffOutput{Body: hackathon}, nil +} + +type UpdateHackathonOutput struct { + Status int +} + +type UpdateHackathonRequest struct { + Name OmittableNullable[string] `json:"name,omitempty"` + Description OmittableNullable[*string] `json:"description,omitempty"` + Location OmittableNullable[*string] `json:"location,omitempty"` + LocationUrl OmittableNullable[*string] `json:"locationUrl,omitempty"` + MaxAttendees OmittableNullable[*int32] `json:"maxAttendees,omitempty"` + ApplicationOpen OmittableNullable[time.Time] `json:"applicationOpen,omitempty"` + ApplicationClose OmittableNullable[time.Time] `json:"applicationClose,omitempty"` + RsvpDeadline OmittableNullable[*time.Time] `json:"rsvpDeadline,omitempty"` + DecisionRelease OmittableNullable[*time.Time] `json:"decisionRelease,omitempty"` + StartTime OmittableNullable[time.Time] `json:"startTime,omitempty"` + EndTime OmittableNullable[time.Time] `json:"endTime,omitempty"` +} + +func (h *handler) handleUpdateHackathon(ctx context.Context, input *struct { + Body UpdateHackathonRequest +}) (*UpdateHackathonOutput, error) { + params := sqlc.UpdateHackathonParams{ + NameDoUpdate: input.Body.Name.Sent, + Name: input.Body.Name.Value, + + DescriptionDoUpdate: input.Body.Description.Sent, + Description: input.Body.Description.Value, + + LocationDoUpdate: input.Body.Location.Sent, + Location: input.Body.Location.Value, + + LocationUrlDoUpdate: input.Body.LocationUrl.Sent, + LocationUrl: input.Body.LocationUrl.Value, + + MaxAttendeesDoUpdate: input.Body.MaxAttendees.Sent, + MaxAttendees: input.Body.MaxAttendees.Value, + + ApplicationOpenDoUpdate: input.Body.ApplicationOpen.Sent, + ApplicationOpen: input.Body.ApplicationOpen.Value, + + ApplicationCloseDoUpdate: input.Body.ApplicationClose.Sent, + ApplicationClose: input.Body.ApplicationClose.Value, + + RsvpDeadlineDoUpdate: input.Body.RsvpDeadline.Sent, + RsvpDeadline: input.Body.RsvpDeadline.Value, + + DecisionReleaseDoUpdate: input.Body.DecisionRelease.Sent, + DecisionRelease: input.Body.DecisionRelease.Value, + + StartTimeDoUpdate: input.Body.StartTime.Sent, + StartTime: input.Body.StartTime.Value, + + EndTimeDoUpdate: input.Body.EndTime.Sent, + EndTime: input.Body.EndTime.Value, + + BannerDoUpdate: false, // Banners are uploaded using a separate endpoint + Banner: nil, + } + + err := h.hackathonService.UpdateHackathon(ctx, params) + + if err != nil { + h.logger.Err(err).Msg("") + return nil, huma.Error500InternalServerError("Failed to get update hackathon") + } + + return &UpdateHackathonOutput{Status: http.StatusOK}, nil +} + +type GetStaffOutput struct { + Body []sqlc.User +} + +func (h *handler) handleGetStaff(ctx context.Context, input *struct{}) (*GetStaffOutput, error) { + staff, err := h.hackathonService.GetStaff(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + return nil, huma.Error500InternalServerError("Failed to get staff") + } + + return &GetStaffOutput{Body: staff}, nil +} + +type GetAttendeesWithDiscordOutput struct { + Body []sqlc.GetAttendeesWithDiscordRow +} + +func (h *handler) handleGetAttendeesWithDiscord(ctx context.Context, input *struct{}) (*GetAttendeesWithDiscordOutput, error) { + attendees, err := h.hackathonService.GetAttendeesWithDiscord(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + return nil, huma.Error500InternalServerError("Failed to get attendees with discord") + } + + return &GetAttendeesWithDiscordOutput{Body: attendees}, nil +} + +type GetAttendeeUserIdsOutput struct { + Body []uuid.UUID +} + +func (h *handler) handleGetAttendeeUserIds(ctx context.Context, input *struct{}) (*GetAttendeeUserIdsOutput, error) { + userIDs, err := h.hackathonService.GetAttendeeUserIds(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + return nil, huma.Error500InternalServerError("Failed to get attendee user ids") + } + + return &GetAttendeeUserIdsOutput{Body: userIDs}, nil +} + +type GetAttendeeCountOutput struct { + Body int64 +} + +func (h *handler) handleGetAttendeeCount(ctx context.Context, input *struct{}) (*GetAttendeeCountOutput, error) { + count, err := h.hackathonService.GetAttendeeCount(ctx) + + if err != nil { + h.logger.Err(err).Msg("") + return nil, huma.Error500InternalServerError("Failed to get attendees count") + } + + return &GetAttendeeCountOutput{Body: count}, nil +} + +type CheckInRequest struct { + UserID uuid.UUID `json:"userID"` + RFID *string `json:"rfid"` +} + +type CheckInOutput struct { + Status int +} + +func (h *handler) handleCheckIn(ctx context.Context, input *struct { + Body CheckInRequest +}) (*CheckInOutput, error) { + if input.Body.RFID != nil { + if *input.Body.RFID == "" { + input.Body.RFID = nil + } + } + + err := h.hackathonService.CheckInAttendee(ctx, input.Body.UserID, input.Body.RFID) + + if err != nil { + h.logger.Err(err).Msg("check in user failed") + return nil, huma.Error500InternalServerError("Failed to check in user") + } + + return &CheckInOutput{Status: http.StatusOK}, nil +} + +type SubmitInterestEmailRequest struct { + Email string `json:"email"` + Source *string `json:"source"` +} + +type SubmitInterestEmailOutput struct { + Status int +} + +func (h *handler) handleSubmitInterestEmail(ctx context.Context, input *struct { + Body SubmitInterestEmailRequest +}) (*SubmitInterestEmailOutput, error) { + if !emailutils.IsValidEmail(input.Body.Email) { + return nil, huma.Error400BadRequest("Invalid email") + } + + _, err := h.hackathonService.SubmitInterestEmail(ctx, input.Body.Email, input.Body.Source) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to submit interest email") + } + + return &SubmitInterestEmailOutput{Status: http.StatusOK}, nil +} + +type UploadBannerOutput struct { + Body *string +} + +func (h *handler) handleUploadBanner(ctx context.Context, input *struct { + RawBody huma.MultipartFormFiles[struct { + Image huma.FormFile `form:"image" contentType:"image/png, image/jpeg, image/jpg" required:"true"` + }] +}) (*UploadBannerOutput, error) { + fileHeader := input.RawBody.Form.File["image"][0] + + if fileHeader.Size > 5*1024*1024 { // 5 MiB + return nil, huma.Error400BadRequest("File too large") + } + + file, err := fileHeader.Open() + + if err != nil { + return nil, huma.Error400BadRequest("Failed to parse uploaded banner image") + } + + url, err := h.hackathonService.UploadBanner(ctx, file, fileHeader) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to upload banner") + } + + return &UploadBannerOutput{Body: url}, nil +} + +type DeleteBannerOutput struct { + Status int +} + +func (h *handler) handleDeleteBanner(ctx context.Context, input *struct{}) (*DeleteBannerOutput, error) { + err := h.hackathonService.DeleteBanner(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to delete banner") + } + + return &DeleteBannerOutput{Status: http.StatusOK}, nil +} diff --git a/apps/api/internal/domains/hackathon/service.go b/apps/api/internal/domains/hackathon/service.go new file mode 100644 index 00000000..dd2ca28e --- /dev/null +++ b/apps/api/internal/domains/hackathon/service.go @@ -0,0 +1,235 @@ +package hackathon + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "mime" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/storage" +) + +type HackathonService struct { + hackathonRepo *repository.HackathonRepository + userRepo *repository.UserRepository + eventInterestsRepo *repository.EventInterestsRepository + logger zerolog.Logger + storage storage.Storage + buckets *config.CoreBuckets +} + +func NewService( + hackathonRepo *repository.HackathonRepository, userRepo *repository.UserRepository, + eventInterestsRepo *repository.EventInterestsRepository, storage storage.Storage, + buckets *config.CoreBuckets, logger zerolog.Logger, +) *HackathonService { + return &HackathonService{ + hackathonRepo: hackathonRepo, + userRepo: userRepo, + eventInterestsRepo: eventInterestsRepo, + storage: storage, + buckets: buckets, + logger: logger.With().Str("service", "HackathonService").Str("domain", "hackathon").Logger(), + } +} + +func (s *HackathonService) CreateHackathon(ctx context.Context, params sqlc.CreateHackathonParams) (*sqlc.Hackathon, error) { + hackathon, err := s.hackathonRepo.CreateHackathon(ctx, params) + + if err != nil { + return nil, errors.New("Failed to create hackathon") + } + + return hackathon, nil +} + +func (s *HackathonService) GetHackathon(ctx context.Context) (*sqlc.Hackathon, error) { + hackathon, err := s.hackathonRepo.GetHackathon(ctx) + + if err != nil { + if errors.Is(err, database.ErrEntityNotFound) { + return nil, database.ErrEntityNotFound + } + s.logger.Err(err).Msg("") + return nil, errors.New("Failed to get hackathon") + } + + return hackathon, nil +} + +func (s *HackathonService) UpdateHackathon(ctx context.Context, params sqlc.UpdateHackathonParams) error { + err := s.hackathonRepo.UpdateHackathon(ctx, params) + + if err != nil { + s.logger.Err(err).Msg("") + return errors.New("Failed to update hackathon") + } + + return nil +} + +func (s *HackathonService) GetStaff(ctx context.Context) ([]sqlc.User, error) { + staff, err := s.hackathonRepo.GetStaff(ctx) + + if err != nil { + return nil, errors.New("Failed to get staff") + } + + if staff == nil { + return []sqlc.User{}, nil + } + + return *staff, nil +} + +func (s *HackathonService) GetAttendeesWithDiscord(ctx context.Context) ([]sqlc.GetAttendeesWithDiscordRow, error) { + attendees, err := s.hackathonRepo.GetAttendeesWithDiscord(ctx) + + if err != nil { + return nil, errors.New("Failed to get attendees with Discord") + } + + if attendees == nil { + return []sqlc.GetAttendeesWithDiscordRow{}, nil + } + + return *attendees, nil +} + +func (s *HackathonService) GetAttendeeUserIds(ctx context.Context) ([]uuid.UUID, error) { + userIDs, err := s.hackathonRepo.GetAttendeeUserIds(ctx) + + if err != nil { + return nil, errors.New("Failed to get attendee user ids") + } + + return userIDs, nil +} + +func (s *HackathonService) GetAttendeeCount(ctx context.Context) (int64, error) { + count, err := s.hackathonRepo.GetAttendeeCount(ctx) + + if err != nil { + return -1, errors.New("Failed to get attendee count") + } + + return count, nil +} + +var ( + ErrRolesNotFound = errors.New("roles not found") + ErrUserNotAttendee = errors.New("user is not an attendee") + ErrUserCheckedIn = errors.New("user already checked in") +) + +func (s *HackathonService) CheckInAttendee(ctx context.Context, userID uuid.UUID, RFID *string) error { + // Retrieve user with their current event role + user, err := s.userRepo.GetUserByID(ctx, userID) + if err != nil { + return repository.ErrUserNotFound + } + + if user.Role != sqlc.UserRoleAttendee { + return ErrUserNotAttendee + } + + if user.CheckedInAt != nil { + return ErrUserCheckedIn + } + + now := time.Now() + // Update user role checked in AND rfid + return s.userRepo.UpdateUser(ctx, sqlc.UpdateUserParams{ + ID: userID, + + Role: sqlc.UserRoleAttendee, + RoleDoUpdate: true, + + CheckedInAt: &now, + CheckedInAtDoUpdate: true, + + Rfid: RFID, + RfidDoUpdate: RFID != nil, + }) +} + +func (s *HackathonService) SubmitInterestEmail(ctx context.Context, email string, source *string) (*sqlc.InterestSubmission, error) { + result, err := s.eventInterestsRepo.AddEmail(ctx, sqlc.AddEmailParams{ + Email: email, + Source: source, + HackathonID: "xii", + }) + if err != nil && errors.Is(err, database.ErrDuplicateEmails) { + return nil, errors.New("Duplicate email") + } else if err != nil { + return nil, errors.New("Failed to submit interest email") + } + + return result, nil +} + +func (s *HackathonService) UploadBanner(ctx context.Context, banner multipart.File, header *multipart.FileHeader) (*string, error) { + bannerFileBuffer := bytes.NewBuffer(nil) + + fileName := header.Filename + fileExt := strings.ToLower(filepath.Ext(fileName)) + + s.logger.Info().Str("Filetype", fileExt).Msg("The file type") + + switch fileExt { + case ".jpg", ".png", ".jpeg": + // Do nothing + default: + return nil, database.ErrUnexpectedFileType + } + + fileType := mime.TypeByExtension(fileExt) + + if fileType == "" { + return nil, database.ErrFailedToUploadBanner + } + + if _, err := io.Copy(bannerFileBuffer, banner); err != nil { + return nil, database.ErrFailedToUploadBanner + } + + uploadKey := fmt.Sprintf("/banner%s", fileExt) + + err := s.storage.Store(ctx, s.buckets.EventAssets, uploadKey, bannerFileBuffer.Bytes(), &fileType) + if err != nil { + return nil, database.ErrFailedToUploadBanner + } + + // Reconstrust URL with cache buster + url := fmt.Sprintf("%s/%s?t=%d", s.buckets.EventAssetsBaseUrl, uploadKey, time.Now().Unix()) + + err = s.hackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ + BannerDoUpdate: true, + Banner: &url, + }) + if err != nil { + return nil, database.ErrFailedToUpdateHackathon + } + + return &url, nil +} + +func (s *HackathonService) DeleteBanner(ctx context.Context) error { + // For now its a soft delete, not actually deleting banner is easiest, just set to null + return s.hackathonRepo.UpdateHackathon(ctx, sqlc.UpdateHackathonParams{ + BannerDoUpdate: true, + Banner: nil, + }) +} diff --git a/apps/api/internal/domains/redeemables/http.go b/apps/api/internal/domains/redeemables/http.go new file mode 100644 index 00000000..12b037fa --- /dev/null +++ b/apps/api/internal/domains/redeemables/http.go @@ -0,0 +1,257 @@ +package redeemables + +import ( + "context" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func RegisterRoutes(redeemablesHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-redeemables", + Method: http.MethodGet, + Summary: "Get Redeemables", + Description: "Returns a list of all redeemable items", + Tags: []string{"Redeemables"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleGetRedeemables) + + huma.Register(group, huma.Operation{ + OperationID: "create-redeemable", + Method: http.MethodPost, + Summary: "Create Redeemable", + Description: "Creates a new redeemable item", + Tags: []string{"Redeemables"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleCreateRedeemable) + + huma.Register(group, huma.Operation{ + OperationID: "update-redeemable", + Method: http.MethodPatch, + Summary: "Update Redeemable", + Description: "Update specific fields (name, stock, max per user) of a redeemable", + Tags: []string{"Redeemables"}, + Path: "/{redeemableId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError, http.StatusBadRequest}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleUpdateRedeemable) + + huma.Register(group, huma.Operation{ + OperationID: "delete-redeemable", + Method: http.MethodDelete, + Summary: "Delete Redeemable", + Description: "Deletes a redeemable by id", + Tags: []string{"Redeemables"}, + Path: "/{redeemableId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError, http.StatusBadRequest}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleDeleteRedeemable) + + huma.Register(group, huma.Operation{ + OperationID: "redeem-redeemable", + Method: http.MethodPost, + Summary: "Redeem Redeemable", + Description: "Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item", + Tags: []string{"Redeemables"}, + Path: "/{redeemableId}/users/{userID}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError, http.StatusBadRequest}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleRedeemRedeemable) + + huma.Register(group, huma.Operation{ + OperationID: "update-redemption", + Method: http.MethodPatch, + Summary: "Update Redemption", + Description: "Updates a redemption created by the user.", + Tags: []string{"Redeemables"}, + Path: "/{redeemableId}/users/{userID}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusInternalServerError, http.StatusBadRequest}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, redeemablesHandler.handleUpdateRedemption) +} + +type handler struct { + redeemablesService *RedeemablesService + config *config.Config + logger zerolog.Logger +} + +func NewHandler(redeemablesService *RedeemablesService, config *config.Config, logger zerolog.Logger) *handler { + return &handler{ + redeemablesService: redeemablesService, + config: config, + logger: logger, + } +} + +type GetRedeemablesOutput struct { + Body *[]sqlc.GetRedeemablesRow +} + +func (h *handler) handleGetRedeemables(ctx context.Context, input *struct{}) (*GetRedeemablesOutput, error) { + redeemables, err := h.redeemablesService.GetRedeemables(ctx) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to get redeemables") + } + + return &GetRedeemablesOutput{Body: redeemables}, nil +} + +type CreateRedeemableRequest struct { + Name string `json:"name" minLength:"1"` + Amount int `json:"amount" minimum:"1"` + MaxUserAmount int `json:"maxUserAmount"` +} + +type CreateRedeemableOutput struct { + Body *sqlc.Redeemable +} + +func (h *handler) handleCreateRedeemable(ctx context.Context, input *struct { + Body CreateRedeemableRequest +}) (*CreateRedeemableOutput, error) { + redeemable, err := h.redeemablesService.CreateRedeemable(ctx, input.Body.Name, input.Body.Amount, input.Body.MaxUserAmount) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to create redeemable") + } + + return &CreateRedeemableOutput{Body: redeemable}, nil +} + +type UpdateRedeemableRequest struct { + Name *string `json:"name,omitempty"` + Amount *int `json:"totalStock,omitempty"` + MaxUserAmount *int `json:"maxUserAmount,omitempty"` +} + +type UpdateRedeemableOutput struct { + Status int +} + +func (h *handler) handleUpdateRedeemable(ctx context.Context, input *struct { + RedeemableId string `path:"redeemableId"` + Body UpdateRedeemableRequest +}) (*UpdateRedeemableOutput, error) { + redeemableId, err := uuid.Parse(input.RedeemableId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid redeemable id") + } + + _, err = h.redeemablesService.UpdateRedeemable(ctx, redeemableId, input.Body.Name, input.Body.Amount, input.Body.MaxUserAmount) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to update redeemable") + } + + return &UpdateRedeemableOutput{Status: http.StatusOK}, nil +} + +type DeleteRedeemableOutput struct { + Status int +} + +func (h *handler) handleDeleteRedeemable(ctx context.Context, input *struct { + RedeemableId string `path:"redeemableId"` +}) (*DeleteRedeemableOutput, error) { + redeemableId, err := uuid.Parse(input.RedeemableId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid redeemable id") + } + + err = h.redeemablesService.DeleteRedeemable(ctx, redeemableId) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to delete redeemable") + } + + return &DeleteRedeemableOutput{Status: http.StatusOK}, nil +} + +type RedeemRedeemableOutput struct { + Status int +} + +func (h *handler) handleRedeemRedeemable(ctx context.Context, input *struct { + RedeemableId string `path:"redeemableId"` +}) (*RedeemRedeemableOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + redeemableId, err := uuid.Parse(input.RedeemableId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid redeemable id") + } + + err = h.redeemablesService.RedeemRedeemable(ctx, redeemableId, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to redeem redeemable") + } + + return &RedeemRedeemableOutput{Status: http.StatusOK}, nil +} + +type UpdateRedemptionRequest struct { + Amount int `json:"newAmount,omitempty"` +} + +type UpdateRedemptionOutput struct { + Status int +} + +func (h *handler) handleUpdateRedemption(ctx context.Context, input *struct { + RedeemableId string `path:"redeemableId"` + Body UpdateRedemptionRequest +}) (*UpdateRedeemableOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + redeemableId, err := uuid.Parse(input.RedeemableId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid redeemable id") + } + + err = h.redeemablesService.UpdateRedemption(ctx, redeemableId, userCtx.UserID, input.Body.Amount) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to update redemption") + } + + return &UpdateRedeemableOutput{Status: http.StatusOK}, nil +} diff --git a/apps/api/internal/services/redeemables.go b/apps/api/internal/domains/redeemables/service.go similarity index 54% rename from apps/api/internal/services/redeemables.go rename to apps/api/internal/domains/redeemables/service.go index c99a797f..a9fd23f2 100644 --- a/apps/api/internal/services/redeemables.go +++ b/apps/api/internal/domains/redeemables/service.go @@ -1,12 +1,12 @@ -package services +package redeemables import ( "context" "github.com/google/uuid" "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) type RedeemablesService struct { @@ -14,26 +14,30 @@ type RedeemablesService struct { logger zerolog.Logger } -func NewRedeemablesService( - redeemablesRepo *repository.RedeemablesRepository, - logger zerolog.Logger) *RedeemablesService { +func NewService(redeemablesRepo *repository.RedeemablesRepository, logger zerolog.Logger) *RedeemablesService { return &RedeemablesService{ redeemablesRepo: redeemablesRepo, logger: logger, } } -func (s *RedeemablesService) GetRedeemablesByEventID(ctx context.Context, eventID uuid.UUID) (*[]sqlc.GetRedeemablesByEventIDRow, error) { - redeemables, err := s.redeemablesRepo.GetRedeemablesByEventID(ctx, eventID) +func (s *RedeemablesService) GetRedeemables(ctx context.Context) (*[]sqlc.GetRedeemablesRow, error) { + redeemables, err := s.redeemablesRepo.GetRedeemables(ctx) if err != nil { - s.logger.Error().Err(err).Msg("failed to get redeemables by event ID") + s.logger.Error().Err(err).Msg("Failed to get redeemables by event ID") return nil, err } return redeemables, nil } -func (s *RedeemablesService) CreateRedeemable(ctx context.Context, eventID uuid.UUID, name string, amount int, maxUserAmount int) (*sqlc.Redeemable, error) { - redeemable, err := s.redeemablesRepo.CreateRedeemable(ctx, eventID, name, amount, maxUserAmount) +func (s *RedeemablesService) CreateRedeemable(ctx context.Context, name string, amount int, maxUserAmount int) (*sqlc.Redeemable, error) { + params := sqlc.CreateRedeemableParams{ + Name: name, + Amount: int32(amount), + MaxUserAmount: int32(maxUserAmount), + HackthonID: "xii", + } + redeemable, err := s.redeemablesRepo.CreateRedeemable(ctx, params) if err != nil { s.logger.Error().Err(err).Msg("failed to create redeemable") return nil, err @@ -51,7 +55,25 @@ func (s *RedeemablesService) DeleteRedeemable(ctx context.Context, redeemableID } func (s *RedeemablesService) UpdateRedeemable(ctx context.Context, redeemableID uuid.UUID, name *string, amount *int, maxUserAmount *int) (*sqlc.Redeemable, error) { - redeemable, err := s.redeemablesRepo.UpdateRedeemable(ctx, redeemableID, name, amount, maxUserAmount) + + var amount32 *int32 + if amount != nil { + v := int32(*amount) + amount32 = &v + } + var maxUserAmount32 *int32 + if maxUserAmount != nil { + v := int32(*maxUserAmount) + maxUserAmount32 = &v + } + + redeemable, err := s.redeemablesRepo.UpdateRedeemable(ctx, sqlc.UpdateRedeemableParams{ + ID: redeemableID, + Name: name, + Amount: amount32, + MaxUserAmount: maxUserAmount32, + }) + if err != nil { s.logger.Error().Err(err).Msg("failed to update redeemable") return nil, err @@ -64,7 +86,10 @@ func (s *RedeemablesService) RedeemRedeemable(ctx context.Context, redeemableID // Probably need event service // CREATE NEW SQL function for getting checked in status - _, err := s.redeemablesRepo.RedeemRedeemable(ctx, redeemableID, userID) + _, err := s.redeemablesRepo.RedeemRedeemable(ctx, sqlc.RedeemRedeemableParams{ + UserID: userID, + RedeemableID: redeemableID, + }) if err != nil { s.logger.Error().Err(err).Msg("failed to redeem redeemable") @@ -74,7 +99,12 @@ func (s *RedeemablesService) RedeemRedeemable(ctx context.Context, redeemableID } func (s *RedeemablesService) UpdateRedemption(ctx context.Context, redeemableID uuid.UUID, userID uuid.UUID, amount int) error { - err := s.redeemablesRepo.UpdateRedemption(ctx, redeemableID, userID, amount) + err := s.redeemablesRepo.UpdateRedemption(ctx, sqlc.UpdateRedemptionParams{ + RedeemableID: redeemableID, + UserID: userID, + Amount: int32(amount), + }) + if err != nil { s.logger.Error().Err(err).Msg("failed to update redemption") return err diff --git a/apps/api/internal/domains/teams/http.go b/apps/api/internal/domains/teams/http.go new file mode 100644 index 00000000..8d22628b --- /dev/null +++ b/apps/api/internal/domains/teams/http.go @@ -0,0 +1,445 @@ +package teams + +import ( + "context" + "errors" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +func RegisterRoutes(teamHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-my-team", + Method: http.MethodGet, + Summary: "Get My Team", + Description: "Returns the team information and the full list of team members for the currently authenticated user", + Tags: []string{"Team"}, + Path: "/me", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleGetMyTeam) + + huma.Register(group, huma.Operation{ + OperationID: "get-team", + Method: http.MethodGet, + Summary: "Get Team", + Description: "Returns the team information and the full list of team members by team id", + Tags: []string{"Team"}, + Path: "/{teamId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleGetTeam) + + huma.Register(group, huma.Operation{ + OperationID: "create-team", + Method: http.MethodPost, + Summary: "Create Team", + Description: "Creates a new team and assigns the user as the owner. Returns the team.", + Tags: []string{"Team"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusConflict, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleCreateTeam) + + huma.Register(group, huma.Operation{ + OperationID: "leave-team", + Method: http.MethodPost, + Summary: "Leave Team", + Description: "Leaves a team if the user is on the team.", + Tags: []string{"Team"}, + Path: "/{teamId}/leave", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleLeaveTeam) + + huma.Register(group, huma.Operation{ + OperationID: "create-join-team-request", + Method: http.MethodPost, + Summary: "Request to Join Team", + Description: "Requests to join a team or fails if user is already on a team.", + Tags: []string{"Team"}, + Path: "/{teamId}/join", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusConflict, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleRequestToJoinTeam) + + huma.Register(group, huma.Operation{ + OperationID: "get-pending-join-team-requests", + Method: http.MethodGet, + Summary: "Get Pending Join Requests for Team", + Description: "Returns a team's pending join requests. This is only allowed for the team's owner.", + Tags: []string{"Team"}, + Path: "/{teamId}/pending-joins", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusForbidden, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleGetPendingRequestsForTeam) + + huma.Register(group, huma.Operation{ + OperationID: "get-my-pending-join-requests", + Method: http.MethodGet, + Summary: "Get User's Pending Join Requests", + Description: "Returns the current user's pending requests for teams.", + Tags: []string{"Team"}, + Path: "/me/pending-joins", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleGetMyPendingRequests) + + huma.Register(group, huma.Operation{ + OperationID: "accept-team-join-request", + Method: http.MethodPost, + Summary: "Accept Team Join Request", + Description: "Accepts a pending team join request. Only the team owner can perform this action.", + Tags: []string{"Team"}, + Path: "/{requestId}/accept", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleAcceptTeamJoinRequest) + + huma.Register(group, huma.Operation{ + OperationID: "reject-team-join-request", + Method: http.MethodPost, + Summary: "Reject Team Join Request", + Description: "Rejects a pending team join request. Only the team owner can perform this action.", + Tags: []string{"Team"}, + Path: "/{requestId}/reject", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleRejectTeamJoinRequest) + + huma.Register(group, huma.Operation{ + OperationID: "kick-member-from-team", + Method: http.MethodPost, + Summary: "Kick Team Member", + Description: "Kicks a member from a team. Only the team owner can perform this action.", + Tags: []string{"Team"}, + Path: "/{teamId}/kick/{memberId}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, teamHandler.handleKickMemberFromTeam) +} + +type handler struct { + teamService *TeamService + logger zerolog.Logger +} + +func NewHandler(teamService *TeamService, logger zerolog.Logger) *handler { + return &handler{ + teamService: teamService, + logger: logger.With().Str("handler", "TeamHandler").Str("domain", "team").Logger(), + } +} + +type GetMyTeamOutput struct { + Body *TeamWithMembers +} + +func (h *handler) handleGetMyTeam(ctx context.Context, input *struct{}) (*GetMyTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + team, err := h.teamService.GetUserTeamWithMembers(ctx, userCtx.UserID) + if err != nil { + return nil, huma.Error500InternalServerError("Fail to get my team") + } + + if team == nil { + return nil, huma.Error404NotFound("user does not have a team") + } + + return &GetMyTeamOutput{Body: team}, nil +} + +type GetTeamOutput struct { + Body *TeamWithMembers +} + +func (h *handler) handleGetTeam(ctx context.Context, input *struct { + TeamId string `path:"teamId"` +}) (*GetTeamOutput, error) { + teamId, err := uuid.Parse(input.TeamId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid team id") + } + + team, err := h.teamService.GetTeamWithMembersByTeamId(ctx, teamId) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to get team by id") + } + + if team == nil { + return nil, huma.Error404NotFound("Could not find team associated with this team id") + } + + return &GetTeamOutput{Body: team}, nil +} + +type CreateTeamRequest struct { + Name string `json:"name"` +} + +type CreateTeamOutput struct { + Body *sqlc.Team +} + +func (h *handler) handleCreateTeam(ctx context.Context, input *struct { + Body CreateTeamRequest +}) (*CreateTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + team, err := h.teamService.CreateTeam(ctx, input.Body.Name, userCtx.UserID) + + if err != nil { + if errors.Is(err, ErrTeamExists) { + return nil, huma.Error409Conflict("User already in a team") + } + + return nil, huma.Error500InternalServerError("Fail to create team") + } + + return &CreateTeamOutput{Body: team}, nil +} + +type LeaveTeamOutput struct { + Status int +} + +func (h *handler) handleLeaveTeam(ctx context.Context, input *struct { + TeamId string `path:"teamId"` +}) (*LeaveTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + teamId, err := uuid.Parse(input.TeamId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid team id") + } + + err = h.teamService.LeaveTeam(ctx, userCtx.UserID, teamId) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to leave team") + } + + return &LeaveTeamOutput{Status: http.StatusOK}, nil +} + +type CreateJoinRequest struct { + Message *string `json:"message"` +} + +type RequestToJoinTeamOutput struct { + Body *sqlc.TeamJoinRequest +} + +func (h *handler) handleRequestToJoinTeam(ctx context.Context, input *struct { + Body CreateJoinRequest + TeamId string `path:"teamId"` +}) (*RequestToJoinTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + teamId, err := uuid.Parse(input.TeamId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid team id") + } + + request, err := h.teamService.RequestToJoinTeam(ctx, teamId, userCtx.UserID, input.Body.Message) + + if err != nil { + if errors.Is(err, ErrUserOnTeam) { + return nil, huma.Error409Conflict("User is already on a team") + } + + return nil, huma.Error500InternalServerError("Fail to create join team request") + } + + return &RequestToJoinTeamOutput{Body: request}, nil +} + +type GetPendingRequestsForTeamOutput struct { + Body []sqlc.ListJoinRequestsByTeamAndStatusWithUserRow +} + +func (h *handler) handleGetPendingRequestsForTeam(ctx context.Context, input *struct { + TeamId string `path:"teamId"` +}) (*GetPendingRequestsForTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + teamId, err := uuid.Parse(input.TeamId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid team id") + } + + requests, err := h.teamService.GetPendingJoinRequestForTeam(ctx, userCtx.UserID, teamId) + + if err != nil { + if errors.Is(err, ErrUserNotTeamOwner) { + return nil, huma.Error403Forbidden("Not authorized to perform this action") + } + + return nil, huma.Error500InternalServerError("Fail to get pending requests for team") + } + + return &GetPendingRequestsForTeamOutput{Body: requests}, nil +} + +type GetMyPendingRequestsOutput struct { + Body []sqlc.TeamJoinRequest +} + +func (h *handler) handleGetMyPendingRequests(ctx context.Context, input *struct{}) (*GetMyPendingRequestsOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + requests, err := h.teamService.GetUserPendingJoinRequests(ctx, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to get user's pending requests") + } + + return &GetMyPendingRequestsOutput{Body: requests}, nil +} + +type AcceptTeamJoinRequestOutput struct { + Status int +} + +func (h *handler) handleAcceptTeamJoinRequest(ctx context.Context, input *struct { + RequestId string `path:"requestId"` +}) (*AcceptTeamJoinRequestOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + requestId, err := uuid.Parse(input.RequestId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid join request id") + } + + err = h.teamService.RespondToJoinRequest(ctx, userCtx.UserID, requestId, true) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to accept team join request") + } + + return &AcceptTeamJoinRequestOutput{Status: http.StatusOK}, nil +} + +type RejectTeamJoinRequestOutput struct { + Status int +} + +func (h *handler) handleRejectTeamJoinRequest(ctx context.Context, input *struct { + RequestId string `path:"requestId"` +}) (*RejectTeamJoinRequestOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + requestId, err := uuid.Parse(input.RequestId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid join request id") + } + + err = h.teamService.RespondToJoinRequest(ctx, userCtx.UserID, requestId, false) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to accept team join request") + } + + return &RejectTeamJoinRequestOutput{Status: http.StatusOK}, nil +} + +type KickMemberFromTeamOutput struct { + Status int +} + +func (h *handler) handleKickMemberFromTeam(ctx context.Context, input *struct { + MemberId string `path:"memberId"` + TeamId string `path:"teamId"` +}) (*KickMemberFromTeamOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + memberId, err := uuid.Parse(input.MemberId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid memberid") + } + + teamId, err := uuid.Parse(input.TeamId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid team id") + } + + err = h.teamService.KickMemberFromTeam(ctx, memberId, teamId, userCtx.UserID) + + if err != nil { + return nil, huma.Error500InternalServerError("Fail to kick member from team") + } + + return &KickMemberFromTeamOutput{Status: http.StatusOK}, nil +} diff --git a/apps/api/internal/services/teams.go b/apps/api/internal/domains/teams/service.go similarity index 61% rename from apps/api/internal/services/teams.go rename to apps/api/internal/domains/teams/service.go index db8844b7..e8d90143 100644 --- a/apps/api/internal/services/teams.go +++ b/apps/api/internal/domains/teams/service.go @@ -1,4 +1,4 @@ -package services +package teams import ( "context" @@ -9,9 +9,9 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/database" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" ) var ( @@ -30,32 +30,35 @@ type TeamService struct { teamRepo *repository.TeamRepository teamMemberRepo *repository.TeamMemberRepository teamJoinRequestRepo *repository.TeamJoinRequestRepository - eventRepo *repository.EventRepository - txm *db.TransactionManager + hackathonRepo *repository.HackathonRepository + userRepo *repository.UserRepository + txm *database.TransactionManager logger zerolog.Logger } -func NewTeamService( +func NewService( teamRepo *repository.TeamRepository, teamMemberRepo *repository.TeamMemberRepository, teamJoinRequestRepo *repository.TeamJoinRequestRepository, - eventRepo *repository.EventRepository, - txm *db.TransactionManager, + hackathonRepo *repository.HackathonRepository, + userRepo *repository.UserRepository, + txm *database.TransactionManager, logger zerolog.Logger) *TeamService { return &TeamService{ teamRepo: teamRepo, teamMemberRepo: teamMemberRepo, teamJoinRequestRepo: teamJoinRequestRepo, - eventRepo: eventRepo, + hackathonRepo: hackathonRepo, + userRepo: userRepo, txm: txm, logger: logger.With().Str("service", "TeamService").Str("component", "team").Logger(), } } // TODO: Remove all join requests after creating a team -func (s *TeamService) CreateTeam(ctx context.Context, name string, eventId, userId uuid.UUID) (*sqlc.Team, error) { +func (s *TeamService) CreateTeam(ctx context.Context, name string, userID uuid.UUID) (*sqlc.Team, error) { // Check if user already has a team for this event. - member, err := s.teamMemberRepo.GetTeamMemberByUserAndEvent(ctx, userId, eventId) + member, err := s.teamMemberRepo.GetTeamMemberByUser(ctx, userID) if err == nil && member != nil { // User already has a team return nil, ErrTeamExists @@ -73,16 +76,26 @@ func (s *TeamService) CreateTeam(ctx context.Context, name string, eventId, user txTeamJoinRequestRepo := s.teamJoinRequestRepo.NewTx(tx) // Delete any pending join requests by the user for this event - if err := txTeamJoinRequestRepo.DeleteByUserAndEventAndStatus(ctx, userId, eventId, sqlc.JoinRequestStatusPENDING); err != nil { + if err := txTeamJoinRequestRepo.DeleteByUserAndStatus(ctx, sqlc.DeleteJoinRequestsByUserAndStatusParams{ + UserID: userID, + Status: sqlc.TeamJoinRequestStatusPending, + }); err != nil { return err } - team, err := txTeamRepo.Create(ctx, name, userId, eventId) + team, err := txTeamRepo.Create(ctx, sqlc.CreateTeamParams{ + Name: name, + OwnerID: &userID, + HackathonID: "xii", + }) if err != nil { return err } - if _, err = txTeamMemberRepo.Create(ctx, team.ID, userId); err != nil { + if _, err = txTeamMemberRepo.Create(ctx, sqlc.CreateTeamMemberParams{ + TeamID: team.ID, + UserID: userID, + }); err != nil { return err } @@ -96,23 +109,22 @@ func (s *TeamService) CreateTeam(ctx context.Context, name string, eventId, user } type MemberWithUserInfo struct { - UserID uuid.UUID `json:"user_id"` - Email *string `json:"email"` - Image *string `json:"image"` - Name string `json:"name"` - JoinedAt *time.Time `json:"joined_at"` + UserID uuid.UUID `json:"userID"` + Email *string `json:"email"` + Image *string `json:"image"` + Name string `json:"name"` + JoinedAt time.Time `json:"joinedAt"` } type TeamWithMembers struct { ID uuid.UUID `json:"id"` - EventId *uuid.UUID `json:"event_id"` - OwnerId *uuid.UUID `json:"owner_id"` + OwnerId *uuid.UUID `json:"ownerId"` Name string `json:"name"` Members []MemberWithUserInfo `json:"members"` } -func (s *TeamService) GetUserTeamWithMembers(ctx context.Context, userId, eventId uuid.UUID) (*TeamWithMembers, error) { - team, err := s.teamRepo.GetTeamByMemberAndEvent(ctx, userId, eventId) +func (s *TeamService) GetUserTeamWithMembers(ctx context.Context, userID uuid.UUID) (*TeamWithMembers, error) { + team, err := s.teamRepo.GetTeamByMember(ctx, userID) if err != nil { // If no team, just return nil if errors.Is(err, repository.ErrTeamNotFound) { @@ -139,7 +151,6 @@ func (s *TeamService) GetUserTeamWithMembers(ctx context.Context, userId, eventI teamWithMembers := TeamWithMembers{ ID: team.ID, - EventId: team.EventID, OwnerId: team.OwnerID, Name: team.Name, Members: parsedMembers, @@ -148,8 +159,12 @@ func (s *TeamService) GetUserTeamWithMembers(ctx context.Context, userId, eventI return &teamWithMembers, nil } -func (s *TeamService) GetTeamsWithMembersByEvent(ctx context.Context, eventId uuid.UUID, limit, offset int32) ([]TeamWithMembers, error) { - teams, err := s.teamRepo.GetTeamsWithMembersByEvent(ctx, eventId, limit, offset) +func (s *TeamService) GetTeamsWithMembers(ctx context.Context, limit, offset int32) ([]TeamWithMembers, error) { + teams, err := s.teamRepo.GetTeamsWithMembers(ctx, sqlc.ListTeamsWithMembersParams{ + Limit: limit, + Offset: offset, + }) + if err != nil { return []TeamWithMembers{}, err } @@ -165,7 +180,6 @@ func (s *TeamService) GetTeamsWithMembersByEvent(ctx context.Context, eventId uu result = append(result, TeamWithMembers{ ID: team.ID, - EventId: team.EventID, OwnerId: team.OwnerID, Name: team.Name, Members: parsedMembers, @@ -175,8 +189,8 @@ func (s *TeamService) GetTeamsWithMembersByEvent(ctx context.Context, eventId uu return result, nil } -func (s *TeamService) GetTeamWithMembers(ctx context.Context, teamId uuid.UUID) (*TeamWithMembers, error) { - team, err := s.teamRepo.GetByID(ctx, teamId) +func (s *TeamService) GetTeamWithMembersByTeamId(ctx context.Context, teamID uuid.UUID) (*TeamWithMembers, error) { + team, err := s.teamRepo.GetByID(ctx, teamID) if err != nil { // If no team, just return nil if errors.Is(err, repository.ErrTeamNotFound) { @@ -203,7 +217,6 @@ func (s *TeamService) GetTeamWithMembers(ctx context.Context, teamId uuid.UUID) teamWithMembers := TeamWithMembers{ ID: team.ID, - EventId: team.EventID, OwnerId: team.OwnerID, Name: team.Name, Members: parsedMember, @@ -212,13 +225,16 @@ func (s *TeamService) GetTeamWithMembers(ctx context.Context, teamId uuid.UUID) return &teamWithMembers, nil } -func (s *TeamService) JoinTeam(ctx context.Context, userId, teamId uuid.UUID) error { - _, err := s.teamMemberRepo.Create(ctx, teamId, userId) +func (s *TeamService) JoinTeam(ctx context.Context, userID, teamID uuid.UUID) error { + _, err := s.teamMemberRepo.Create(ctx, sqlc.CreateTeamMemberParams{ + UserID: userID, + TeamID: teamID, + }) return err } -func (s *TeamService) LeaveTeam(ctx context.Context, userId, teamId uuid.UUID) error { - team, err := s.teamRepo.GetByID(ctx, teamId) +func (s *TeamService) LeaveTeam(ctx context.Context, userID, teamID uuid.UUID) error { + team, err := s.teamRepo.GetByID(ctx, teamID) if err != nil { if errors.Is(err, repository.ErrTeamNotFound) { return ErrTeamNotFound @@ -227,8 +243,11 @@ func (s *TeamService) LeaveTeam(ctx context.Context, userId, teamId uuid.UUID) e } // Check if user is NOT the owner - if team.OwnerID == nil || *team.OwnerID != userId { - return s.teamMemberRepo.Delete(ctx, team.ID, userId) + if team.OwnerID == nil || *team.OwnerID != userID { + return s.teamMemberRepo.Delete(ctx, sqlc.RemoveTeamMemberParams{ + TeamID: teamID, + UserID: userID, + }) } // User IS the owner @@ -249,7 +268,10 @@ func (s *TeamService) LeaveTeam(ctx context.Context, userId, teamId uuid.UUID) e txTeamRepo := s.teamRepo.NewTx(tx) txTeamMemberRepo := s.teamMemberRepo.NewTx(tx) - if err := txTeamMemberRepo.Delete(ctx, team.ID, userId); err != nil { + if err := txTeamMemberRepo.Delete(ctx, sqlc.RemoveTeamMemberParams{ + TeamID: teamID, + UserID: userID, + }); err != nil { return err } return txTeamRepo.Delete(ctx, team.ID) @@ -259,7 +281,7 @@ func (s *TeamService) LeaveTeam(ctx context.Context, userId, teamId uuid.UUID) e // Choose the next owner deterministically var nextOwner sqlc.GetTeamMembersRow for _, m := range members { - if m.UserID != userId { + if m.UserID != userID { nextOwner = m break } @@ -275,18 +297,26 @@ func (s *TeamService) LeaveTeam(ctx context.Context, userId, teamId uuid.UUID) e txTeamRepo := s.teamRepo.NewTx(tx) txTeamMemberRepo := s.teamMemberRepo.NewTx(tx) - if err := txTeamMemberRepo.Delete(ctx, team.ID, userId); err != nil { + if err := txTeamMemberRepo.Delete(ctx, sqlc.RemoveTeamMemberParams{ + TeamID: teamID, + UserID: userID, + }); err != nil { return err } - _, err := txTeamRepo.Update(ctx, team.ID, nil, &nextOwner.UserID) + // _, err := txTeamRepo.Update(ctx, team.ID, nil, &nextOwner.UserID) + _, err := txTeamRepo.Update(ctx, sqlc.UpdateTeamByIdParams{ + ID: team.ID, + OwnerIDDoUpdate: true, + OwnerID: &nextOwner.UserID, + }) return err }) } -func (s *TeamService) RequestToJoinTeam(ctx context.Context, eventId, teamId, userId uuid.UUID, message *string) (*sqlc.TeamJoinRequest, error) { +func (s *TeamService) RequestToJoinTeam(ctx context.Context, teamID, userID uuid.UUID, message *string) (*sqlc.TeamJoinRequest, error) { // Ensure user is not already on a team (User's can't request to join a team if they are already on a team) - _, err := s.teamMemberRepo.GetTeamMemberByUserAndEvent(ctx, userId, eventId) + _, err := s.teamMemberRepo.GetTeamMemberByUser(ctx, userID) if err != nil && !errors.Is(err, repository.ErrTeamMemberNotFound) { return nil, err } @@ -295,7 +325,12 @@ func (s *TeamService) RequestToJoinTeam(ctx context.Context, eventId, teamId, us return nil, ErrUserOnTeam } - request, err := s.teamJoinRequestRepo.Create(ctx, teamId, userId, message) + request, err := s.teamJoinRequestRepo.Create(ctx, sqlc.CreateTeamJoinRequestParams{ + TeamID: teamID, + UserID: userID, + RequestMessage: message, + }) + if err != nil { return nil, err } @@ -303,18 +338,22 @@ func (s *TeamService) RequestToJoinTeam(ctx context.Context, eventId, teamId, us return request, nil } -func (s *TeamService) GetPendingJoinRequestForTeam(ctx context.Context, userId, teamId uuid.UUID) ([]sqlc.ListJoinRequestsByTeamAndStatusWithUserRow, error) { +func (s *TeamService) GetPendingJoinRequestForTeam(ctx context.Context, userID, teamID uuid.UUID) ([]sqlc.ListJoinRequestsByTeamAndStatusWithUserRow, error) { // Get team and check if user is the owner of the tema - team, err := s.teamRepo.GetByID(ctx, teamId) + team, err := s.teamRepo.GetByID(ctx, teamID) if err != nil { return []sqlc.ListJoinRequestsByTeamAndStatusWithUserRow{}, err } - if *team.OwnerID != userId { + if *team.OwnerID != userID { return []sqlc.ListJoinRequestsByTeamAndStatusWithUserRow{}, ErrUserNotTeamOwner } - requests, err := s.teamJoinRequestRepo.ListJoinRequestsByTeamWithUser(ctx, teamId, sqlc.JoinRequestStatusPENDING) + requests, err := s.teamJoinRequestRepo.ListJoinRequestsByTeamWithUser(ctx, sqlc.ListJoinRequestsByTeamAndStatusWithUserParams{ + TeamID: teamID, + Status: sqlc.TeamJoinRequestStatusPending, + }) + if err != nil { return []sqlc.ListJoinRequestsByTeamAndStatusWithUserRow{}, err } @@ -322,8 +361,12 @@ func (s *TeamService) GetPendingJoinRequestForTeam(ctx context.Context, userId, return requests, nil } -func (s *TeamService) GetUserPendingJoinRequestsByEvent(ctx context.Context, userId, eventId uuid.UUID) ([]sqlc.TeamJoinRequest, error) { - requests, err := s.teamJoinRequestRepo.ListJoinRequestsByUserAndEvent(ctx, userId, eventId, sqlc.JoinRequestStatusPENDING) +func (s *TeamService) GetUserPendingJoinRequests(ctx context.Context, userID uuid.UUID) ([]sqlc.TeamJoinRequest, error) { + requests, err := s.teamJoinRequestRepo.ListJoinRequestsByUserAndStatus(ctx, sqlc.ListTeamJoinRequestsByUserAndStatusParams{ + UserID: userID, + Status: sqlc.TeamJoinRequestStatusPending, + }) + if err != nil { return []sqlc.TeamJoinRequest{}, err } @@ -331,9 +374,9 @@ func (s *TeamService) GetUserPendingJoinRequestsByEvent(ctx context.Context, use return requests, nil } -func (s *TeamService) RespondToJoinRequest(ctx context.Context, ownerId, requestId uuid.UUID, accept bool) error { +func (s *TeamService) RespondToJoinRequest(ctx context.Context, ownerID, requestID uuid.UUID, accept bool) error { // Retrieve the join request - oldRequest, err := s.teamJoinRequestRepo.GetById(ctx, requestId) + oldRequest, err := s.teamJoinRequestRepo.GetById(ctx, requestID) if err != nil { return err } @@ -343,25 +386,28 @@ func (s *TeamService) RespondToJoinRequest(ctx context.Context, ownerId, request if err != nil { return err } - if *team.OwnerID != ownerId { + if *team.OwnerID != ownerID { return ErrUserNotTeamOwner } // Also ensure user is actually an applicant or attendee - role, err := s.eventRepo.GetEventRoleByIds(ctx, oldRequest.UserID, *team.EventID) + user, err := s.userRepo.GetUserByID(ctx, oldRequest.UserID) if err != nil { - if errors.Is(err, repository.ErrEventRoleNotFound) { + if errors.Is(err, database.ErrEntityNotFound) { return ErrUserNotApplicantOrAttendee } return err } - if role.Role != sqlc.EventRoleTypeAttendee && role.Role != sqlc.EventRoleTypeApplicant { + if user == nil { + return ErrUserNotApplicantOrAttendee + } + if user.Role != sqlc.UserRoleAttendee && user.Role != sqlc.UserRoleApplicant { return ErrUserNotApplicantOrAttendee } // Ensure user is not already on a team - _, err = s.teamMemberRepo.GetTeamMemberByUserAndEvent(ctx, oldRequest.UserID, *team.EventID) + _, err = s.teamMemberRepo.GetTeamMemberByUser(ctx, oldRequest.UserID) if err != nil && !errors.Is(err, repository.ErrTeamMemberNotFound) { return err } @@ -386,40 +432,57 @@ func (s *TeamService) RespondToJoinRequest(ctx context.Context, ownerId, request txTeamMemberRepo := s.teamMemberRepo.NewTx(tx) txTeamJoinRequestRepo := s.teamJoinRequestRepo.NewTx(tx) - if _, err := txTeamMemberRepo.Create(ctx, oldRequest.TeamID, oldRequest.UserID); err != nil { + if _, err := txTeamMemberRepo.Create(ctx, sqlc.CreateTeamMemberParams{ + TeamID: oldRequest.TeamID, + UserID: oldRequest.UserID, + }); err != nil { return err } - if _, err := txTeamJoinRequestRepo.UpdateStatus(ctx, requestId, sqlc.JoinRequestStatusAPPROVED); err != nil { + if _, err := txTeamJoinRequestRepo.UpdateStatus(ctx, sqlc.UpdateTeamJoinRequestParams{ + ID: requestID, + StatusDoUpdate: true, + Status: sqlc.TeamJoinRequestStatusApproved, + }); err != nil { return err } // Delete all other pending requests by the user for this event - return txTeamJoinRequestRepo.DeleteByUserAndEventAndStatus(ctx, oldRequest.UserID, *team.EventID, sqlc.JoinRequestStatusPENDING) + return txTeamJoinRequestRepo.DeleteByUserAndStatus(ctx, sqlc.DeleteJoinRequestsByUserAndStatusParams{ + UserID: oldRequest.UserID, + Status: sqlc.TeamJoinRequestStatusPending, + }) }) } else { // Rejecting the request: just update request status - _, err := s.teamJoinRequestRepo.UpdateStatus(ctx, requestId, sqlc.JoinRequestStatusREJECTED) + _, err := s.teamJoinRequestRepo.UpdateStatus(ctx, sqlc.UpdateTeamJoinRequestParams{ + ID: requestID, + StatusDoUpdate: true, + Status: sqlc.TeamJoinRequestStatusApproved, + }) return err } } -func (s *TeamService) KickMemberFromTeam(ctx context.Context, memberId, teamId, userId uuid.UUID) error { - team, err := s.teamRepo.GetByID(ctx, teamId) +func (s *TeamService) KickMemberFromTeam(ctx context.Context, memberID, teamID, userID uuid.UUID) error { + team, err := s.teamRepo.GetByID(ctx, teamID) if err != nil { return err } - if *team.OwnerID != userId { - s.logger.Warn().Str("team_owner", team.OwnerID.String()).Str("user", userId.String()).Msg("User attempting to kick member is not the team owner") + if *team.OwnerID != userID { + s.logger.Warn().Str("team_owner", team.OwnerID.String()).Str("user", userID.String()).Msg("User attempting to kick member is not the team owner") return ErrUserNotTeamOwner } // Prevent owner from kicking themselves - if memberId == userId { + if memberID == userID { s.logger.Warn().Str("team_owner", team.OwnerID.String()).Msg("Team owner attempting to kick themselves") return ErrKickOwnerSelf } - return s.teamMemberRepo.Delete(ctx, team.ID, memberId) + return s.teamMemberRepo.Delete(ctx, sqlc.RemoveTeamMemberParams{ + TeamID: teamID, + UserID: memberID, + }) } diff --git a/apps/api/internal/domains/users/http.go b/apps/api/internal/domains/users/http.go new file mode 100644 index 00000000..b968ccb8 --- /dev/null +++ b/apps/api/internal/domains/users/http.go @@ -0,0 +1,508 @@ +package users + +import ( + "context" + "errors" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/api/cookie" + "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/ctxutils" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/emailutils" +) + +func RegisterRoutes(userHandler *handler, group huma.API, mw *middleware.Middleware) { + huma.Register(group, huma.Operation{ + OperationID: "get-me", + Method: http.MethodGet, + Summary: "Get Me", + Description: "Returns the authenticated user's profile", + Tags: []string{"Users"}, + Path: "/me", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleGetMe) + + huma.Register(group, huma.Operation{ + OperationID: "get-users", + Method: http.MethodGet, + Summary: "Get Users", + Description: "Get or search for users by name or email. If no search term is provided, returns all users with pagination.", + Tags: []string{"Users"}, + Path: "", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleGetUsers) + + huma.Register(group, huma.Operation{ + OperationID: "get-user-by-id", + Method: http.MethodGet, + Summary: "Get User By Id", + Description: "Returns the user associated with the user id", + Tags: []string{"Users"}, + Path: "/userid/{userID}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleGetUserById) + + huma.Register(group, huma.Operation{ + OperationID: "get-user-by-email", + Method: http.MethodGet, + Summary: "Get User By Email", + Description: "Returns the user associated with the email", + Tags: []string{"Users"}, + Path: "/email/{email}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleGetUserByEmail) + + huma.Register(group, huma.Operation{ + OperationID: "get-user-by-rfid", + Method: http.MethodGet, + Summary: "Get User By RFID", + Description: "Returns the user associated with the RFID", + Tags: []string{"Users"}, + Path: "/rfid/{rfid}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireStaffHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleGetUserByRFID) + + huma.Register(group, huma.Operation{ + OperationID: "update-email-consent", + Method: http.MethodPatch, + Summary: "Update Email Consent", + Description: "Updates the user's email consent setting", + Tags: []string{"Users"}, + Path: "/me/email-consent", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, userHandler.handleUpdateEmailConsent) + + huma.Register(group, huma.Operation{ + OperationID: "update-user", + Method: http.MethodPatch, + Summary: "Update User", + Description: "Updates information of the authenticated user", + Tags: []string{"Users"}, + Path: "/me", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, userHandler.handleUpdateUser) + + huma.Register(group, huma.Operation{ + OperationID: "onboard-user", + Method: http.MethodPatch, + Summary: "Onboard User", + Description: "Allows the user to submit information such as name and preferred email, and complete the onboarding process", + Tags: []string{"Users"}, + Path: "/me/onboarding", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusNotFound, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + DefaultStatus: http.StatusOK, + }, userHandler.handleOnboarding) + + huma.Register(group, huma.Operation{ + OperationID: "assign-role", + Method: http.MethodPost, + Summary: "Assign Role", + Description: "Assigns/modify a user's role", + Tags: []string{"Users"}, + Path: "/roles/assign", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleAssignRole) + + huma.Register(group, huma.Operation{ + OperationID: "batch-assign-roles", + Method: http.MethodPost, + Summary: "Batch Assign Roles", + Description: "Batch assign/modify multiple users' roles", + Tags: []string{"Users"}, + Path: "/roles/batch-assign", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleBatchAssignRoles) + + huma.Register(group, huma.Operation{ + OperationID: "revoke-role", + Method: http.MethodPost, + Summary: "Revoke Role", + Description: "Remove a user's role", + Tags: []string{"Users"}, + Path: "/roles/revoke/{userID}", + Middlewares: huma.Middlewares{mw.Auth.RequireAuthHuma, mw.Auth.RequireAdminHuma}, + Errors: []int{http.StatusUnauthorized, http.StatusBadRequest, http.StatusInternalServerError}, + Parameters: []*huma.Param{cookie.SessionCookieHumaParam}, + }, userHandler.handleRevokeEventRole) +} + +type handler struct { + userService *UserService + config *config.Config + logger zerolog.Logger +} + +func NewHandler(userService *UserService, config *config.Config, logger zerolog.Logger) *handler { + return &handler{ + userService: userService, + config: config, + logger: logger.With().Str("handler", "UserHandler").Str("domain", "user").Logger(), + } +} + +type GetMeOutput struct { + Body *middleware.UserContext +} + +func (h *handler) handleGetMe(ctx context.Context, input *struct{}) (*GetMeOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + return &GetMeOutput{Body: userCtx}, nil +} + +type GetUserByEmailOutput struct { + Body *sqlc.User +} + +func (h *handler) handleGetUserByEmail(ctx context.Context, input *struct { + Email string `path:"email"` +}) (*GetUserByEmailOutput, error) { + if !emailutils.IsValidEmail(input.Email) { + return nil, huma.Error400BadRequest("Invalid email") + } + + user, err := h.userService.GetUserByEmail(ctx, input.Email) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to get user by email") + } + + if user == nil { + return nil, huma.Error404NotFound("User not found") + } + + return &GetUserByEmailOutput{Body: user}, nil +} + +type GetUserByIdOutput struct { + Body *sqlc.User +} + +func (h *handler) handleGetUserById(ctx context.Context, input *struct { + UserId string `path:"userID"` +}) (*GetUserByIdOutput, error) { + userID, err := uuid.Parse(input.UserId) + + if err != nil { + return nil, huma.Error400BadRequest("Invalid user id") + } + + user, err := h.userService.GetUserById(ctx, userID) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to get user by id") + } + + if user == nil { + return nil, huma.Error404NotFound("User not found") + } + + return &GetUserByIdOutput{Body: user}, nil +} + +type GetUserByRFIDOutput struct { + Body *sqlc.User +} + +func (h *handler) handleGetUserByRFID(ctx context.Context, input *struct { + RFID string `path:"rfid"` +}) (*GetUserByRFIDOutput, error) { + user, err := h.userService.GetUserByRFID(ctx, input.RFID) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to get user by rfid") + } + + if user == nil { + return nil, huma.Error404NotFound("User not found") + } + + return &GetUserByRFIDOutput{Body: user}, nil +} + +type UpdateUserOutput struct { + Status int +} + +type UpdateUserRequest struct { + Name string `json:"name"` + PreferredEmail string `json:"preferredEmail"` +} + +func (h *handler) handleUpdateUser(ctx context.Context, input *struct { + Body UpdateUserRequest +}) (*UpdateUserOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + if input.Body.Name == "" { + return nil, huma.Error400BadRequest("Name is required") + } + + if input.Body.PreferredEmail != "" && !emailutils.IsValidEmail(input.Body.PreferredEmail) { + return nil, huma.Error400BadRequest("Invalid email format") + } + + // TODO: Allow/add more fields here + params := sqlc.UpdateUserParams{ + ID: userCtx.UserID, + NameDoUpdate: true, + Name: input.Body.Name, + PreferredEmailDoUpdate: true, + PreferredEmail: &input.Body.PreferredEmail, + } + + err := h.userService.UpdateUser(ctx, userCtx.UserID, params) + if err != nil { + h.logger.Err(err).Msg("failed to update user") + if errors.Is(err, ErrUserNotFound) { + return nil, huma.Error404NotFound("User not found") + } else { + return nil, huma.Error500InternalServerError("failed to update user") + } + } + + res := &UpdateUserOutput{ + Status: http.StatusOK, + } + + return res, nil +} + +type UpdateEmailConsentOutput struct { + Status int +} + +type UpdateEmailConsentRequest struct { + EmailConsent bool `json:"emailConsent"` +} + +func (h *handler) handleUpdateEmailConsent(ctx context.Context, input *struct { + Body UpdateEmailConsentRequest +}) (*UpdateEmailConsentOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + params := sqlc.UpdateUserParams{ + EmailConsentDoUpdate: true, + EmailConsent: input.Body.EmailConsent, + } + + err := h.userService.UpdateUser(ctx, userCtx.UserID, params) + + if err != nil { + h.logger.Err(err).Msg("failed to update email consent") + if errors.Is(err, ErrUserNotFound) { + return nil, huma.Error404NotFound("User not found") + } else { + return nil, huma.Error500InternalServerError("Failed to update email consent") + } + } + + res := &UpdateEmailConsentOutput{ + Status: http.StatusOK, + } + + return res, nil +} + +type OnboardingOutput struct { + Status int +} + +type OnboardingRequest struct { + Name string `json:"name"` + PreferredEmail string `json:"preferredEmail"` +} + +func (h *handler) handleOnboarding(ctx context.Context, input *struct { + Body OnboardingRequest +}) (*OnboardingOutput, error) { + userCtx := ctxutils.GetUserFromCtx(ctx) + + if userCtx == nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + if input.Body.Name == "" || input.Body.PreferredEmail == "" { + return nil, huma.Error400BadRequest("Name and preferred email are required") + } + + if input.Body.PreferredEmail != "" && !emailutils.IsValidEmail(input.Body.PreferredEmail) { + return nil, huma.Error400BadRequest("Invalid email format") + } + + err := h.userService.CompleteOnboarding(ctx, userCtx.UserID, input.Body.Name, input.Body.PreferredEmail) + + if err != nil { + h.logger.Err(err).Msg("failed to complete onboarding") + if errors.Is(err, ErrUserNotFound) { + return nil, huma.Error404NotFound("User not found") + } else { + return nil, huma.Error500InternalServerError("failed to complete onboarding") + } + } + + res := &OnboardingOutput{ + Status: http.StatusOK, + } + + return res, nil +} + +type GetUsersOutput struct { + Body *[]sqlc.User +} + +func (h *handler) handleGetUsers(ctx context.Context, input *struct { + Search string `query:"search"` + Limit int `query:"limit" default:"50"` + Offset int `query:"offset" default:"0"` +}) (*GetUsersOutput, error) { + var searchTerm *string + if input.Search == "" { + searchTerm = nil + } else { + searchTerm = &input.Search + } + + users, err := h.userService.GetAllUsers(ctx, searchTerm, int32(input.Limit), int32(input.Offset)) + + if err != nil { + h.logger.Err(err).Msg("Failed to retrieve users") + return nil, huma.Error500InternalServerError("Failed to retrieve users") + } + + res := &GetUsersOutput{Body: &users} + + return res, nil +} + +type AssignRoleRequest struct { + Email *string `json:"email"` + UserID *string `json:"userID"` + Role sqlc.UserRole `json:"role"` +} + +type AssignRoleOutput struct { + Status int +} + +func (h *handler) handleAssignRole(ctx context.Context, input *struct { + Body AssignRoleRequest +}) (*AssignRoleOutput, error) { + var userID *uuid.UUID + + if input.Body.UserID == nil { + userID = nil + } else { + userIDTemp, err := uuid.Parse(*input.Body.UserID) + + if err != nil { + userID = nil + } else { + userID = &userIDTemp + } + } + + err := h.userService.AssignRole(ctx, userID, input.Body.Email, input.Body.Role) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to assign role") + } + + return &AssignRoleOutput{Status: http.StatusOK}, nil +} + +type AssignRoleBatchRequest struct { + Assignments []AssignRoleRequest `json:"assignments"` +} + +type BatchAssignRolesOutput struct { + Status int +} + +func (h *handler) handleBatchAssignRoles(ctx context.Context, input *struct { + Body AssignRoleBatchRequest +}) (*BatchAssignRolesOutput, error) { + for _, assignment := range input.Body.Assignments { + userID := ParseUUIDOrNil(assignment.UserID) + + err := h.userService.AssignRole(ctx, userID, assignment.Email, assignment.Role) + if err != nil { + return nil, huma.Error500InternalServerError("Failed to batch assign roles") + } + } + + return &BatchAssignRolesOutput{Status: http.StatusOK}, nil +} + +type RevokeEventRoleOutput struct { + Status int +} + +func (h *handler) handleRevokeEventRole(ctx context.Context, input *struct { + UserId string `path:"userID"` +}) (*RevokeEventRoleOutput, error) { + userID, err := uuid.Parse(input.UserId) + + if err != nil { + return nil, huma.Error400BadRequest("Failed to get current user info") + } + + err = h.userService.RevokeRole(ctx, userID) + + if err != nil { + return nil, huma.Error500InternalServerError("Failed to revoke role") + } + + return &RevokeEventRoleOutput{Status: http.StatusOK}, nil +} + +func ParseUUIDOrNil(s *string) *uuid.UUID { + if s == nil || *s == "" { + return nil + } + id, err := uuid.Parse(*s) + if err != nil { + return nil + } + return &id +} diff --git a/apps/api/internal/domains/users/service.go b/apps/api/internal/domains/users/service.go new file mode 100644 index 00000000..792cc822 --- /dev/null +++ b/apps/api/internal/domains/users/service.go @@ -0,0 +1,228 @@ +package users + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/database/repository" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrFailedToUpdateUser = errors.New("failed to update user") + ErrFailedToGetUser = errors.New("failed to get user") +) + +type UserService struct { + userRepo *repository.UserRepository + logger zerolog.Logger +} + +func NewService(userRepo *repository.UserRepository, logger zerolog.Logger) *UserService { + return &UserService{ + userRepo: userRepo, + logger: logger.With().Str("service", "UserService").Str("domain", "user").Logger(), + } +} + +func (s *UserService) GetUserById(ctx context.Context, userID uuid.UUID) (*sqlc.User, error) { + user, err := s.userRepo.GetUserByID(ctx, userID) + + if err != nil { + if err == repository.ErrUserNotFound { + return nil, ErrUserNotFound + } else { + s.logger.Err(err).Msg("failed to get user by ID") + return nil, ErrFailedToGetUser + } + } + + return user, nil +} + +func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*sqlc.User, error) { + user, err := s.userRepo.GetUserByEmail(ctx, email) + + if err != nil { + if err == repository.ErrUserNotFound { + return nil, ErrUserNotFound + } else { + s.logger.Err(err).Msg("get user by email fail") + return nil, ErrFailedToGetUser + } + } + + return user, nil +} + +func (s *UserService) GetUserEmailInfoById(ctx context.Context, userID uuid.UUID) (*sqlc.GetUserEmailInfoByIdRow, error) { + emailInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userID) + + if err != nil { + if err == repository.ErrUserNotFound { + return nil, ErrUserNotFound + } else { + s.logger.Err(err).Msg("get user by email fail") + return nil, ErrFailedToGetUser + } + } + + return emailInfo, nil +} + +func (s *UserService) GetUserByRFID(ctx context.Context, rfid string) (*sqlc.User, error) { + user, err := s.userRepo.GetUserByRFID(ctx, rfid) + + if err != nil { + if err == repository.ErrUserNotFound { + return nil, ErrUserNotFound + } else { + s.logger.Err(err).Msg("get user by rfid fail") + return nil, ErrFailedToGetUser + } + } + + return user, nil +} + +// func (s *UserService) GetCheckedInStatusByUserId(ctx context.Context, userID uuid.UUID) (bool, error) { +// checkedIn, err := s.userRepo.GetCheckedInStatusByUserId(ctx, userID) + +// if err != nil { +// s.logger.Err(err).Msg("check in status fail") +// return false, errors.New("Failed to get check in status for user") +// } + +// return checkedIn, nil +// } + +func (s *UserService) UpdateUser(ctx context.Context, userID uuid.UUID, params sqlc.UpdateUserParams) error { + params.ID = userID + + err := s.userRepo.UpdateUser(ctx, params) + if err != nil { + if err == repository.ErrUserNotFound { + s.logger.Err(err).Msg(repository.ErrUserNotFound.Error()) + return ErrUserNotFound + } else { + s.logger.Err(err).Msg("failed to update user") + return ErrFailedToUpdateUser + } + } + + return nil +} + +func (s *UserService) CompleteOnboarding(ctx context.Context, userID uuid.UUID, name, email string) error { + params := sqlc.UpdateUserParams{ + ID: userID, + NameDoUpdate: true, + Name: name, + PreferredEmailDoUpdate: true, + PreferredEmail: &email, + OnboardedDoUpdate: true, + Onboarded: true, + } + + return s.UpdateUser(ctx, userID, params) +} + +func (s *UserService) GetAllUsers(ctx context.Context, search *string, limit, offset int32) ([]sqlc.User, error) { + users, err := s.userRepo.GetAllUsers(ctx, sqlc.GetUsersParams{ + Limit: limit, + Offset: offset, + Search: search, + }) + + if err != nil { + s.logger.Err(err).Msg("get all users fail") + return []sqlc.User{}, errors.New("Failed to get users") + } + + return users, nil +} + +// func (s *UserService) GetRole(ctx context.Context, userID uuid.UUID) (*sqlc.RoleType, error) { +// role, err := s.eventRolesRepo.GetRoleByUserId(ctx, userID) + +// if err != nil { +// return nil, err +// } + +// return role, nil +// } + +func (s *UserService) AssignRole(ctx context.Context, userID *uuid.UUID, email *string, role sqlc.UserRole) error { + if userID == nil && email == nil { + return errors.New("must provide either userID or email") + } + + var selectedUser *sqlc.User + var err error + + if userID != nil { + selectedUser, err = s.userRepo.GetUserByID(ctx, *userID) + // Do not return if user not found, the query needs to fallback to other optiosn + if err != nil && !errors.Is(err, repository.ErrUserNotFound) { + s.logger.Err(err).Msg("Something went wrong getting by id") + return err + } + } + + if selectedUser == nil && email != nil { + selectedUser, err = s.userRepo.GetUserByEmail(ctx, *email) + if err != nil { + s.logger.Err(err).Msg("Something went wrong getting by email") + return err + } + } + + // Just a double safety check (should usually be caught by queries above) + if selectedUser == nil { + s.logger.Warn().Msg(("User not found from email OR id")) + return repository.ErrUserNotFound + } + + // Now assign the event role + err = s.userRepo.UpdateRole(ctx, sqlc.UpdateRoleParams{ + UserID: selectedUser.ID, + Role: role, + }) + if err != nil { + return err + } + + return nil +} + +func (s *UserService) RevokeRole(ctx context.Context, userID uuid.UUID) error { + return s.userRepo.RemoveRole(ctx, userID) +} + +func (s *UserService) UpdateRole(ctx context.Context, userID uuid.UUID, role sqlc.UserRole) error { + return s.userRepo.UpdateRole(ctx, sqlc.UpdateRoleParams{ + UserID: userID, + Role: role, + }) +} + +// func (s *UserService) UpdateRoleById(ctx context.Context, userID uuid.UUID, role *sqlc.RoleType, checkedInAt *time.Time, RFID *string) error { +// if role == nil && checkedInAt == nil && RFID == nil { +// return errors.New("no fields provided to update") +// } + +// return s.eventRolesRepo.UpdateRoleByUserId(ctx, sqlc.UpdateRoleByUserIdParams{ +// UserID: userID, +// Role: *role, +// RoleDoUpdate: role != nil, + +// CheckedInAt: checkedInAt, +// CheckedInAtDoUpdate: checkedInAt != nil, + +// Rfid: RFID, +// RfidDoUpdate: RFID != nil, +// }) +// } diff --git a/apps/api/internal/email/ses.go b/apps/api/internal/emailutils/ses.go similarity index 99% rename from apps/api/internal/email/ses.go rename to apps/api/internal/emailutils/ses.go index e234a044..dbfb9749 100644 --- a/apps/api/internal/email/ses.go +++ b/apps/api/internal/emailutils/ses.go @@ -1,4 +1,4 @@ -package email +package emailutils import ( "context" diff --git a/apps/api/internal/email/templates/ApplicationAcceptedEmail.html b/apps/api/internal/emailutils/templates/ApplicationAcceptedEmail.html similarity index 100% rename from apps/api/internal/email/templates/ApplicationAcceptedEmail.html rename to apps/api/internal/emailutils/templates/ApplicationAcceptedEmail.html diff --git a/apps/api/internal/email/templates/ApplicationRejectedEmail.html b/apps/api/internal/emailutils/templates/ApplicationRejectedEmail.html similarity index 100% rename from apps/api/internal/email/templates/ApplicationRejectedEmail.html rename to apps/api/internal/emailutils/templates/ApplicationRejectedEmail.html diff --git a/apps/api/internal/email/templates/ConfirmationEmail.html b/apps/api/internal/emailutils/templates/ConfirmationEmail.html similarity index 100% rename from apps/api/internal/email/templates/ConfirmationEmail.html rename to apps/api/internal/emailutils/templates/ConfirmationEmail.html diff --git a/apps/api/internal/email/templates/WaitlistAcceptanceEmail.html b/apps/api/internal/emailutils/templates/WaitlistAcceptanceEmail.html similarity index 100% rename from apps/api/internal/email/templates/WaitlistAcceptanceEmail.html rename to apps/api/internal/emailutils/templates/WaitlistAcceptanceEmail.html diff --git a/apps/api/internal/email/templates/WelcomeEmail.html b/apps/api/internal/emailutils/templates/WelcomeEmail.html similarity index 100% rename from apps/api/internal/email/templates/WelcomeEmail.html rename to apps/api/internal/emailutils/templates/WelcomeEmail.html diff --git a/apps/api/internal/email/validation.go b/apps/api/internal/emailutils/validation.go similarity index 85% rename from apps/api/internal/email/validation.go rename to apps/api/internal/emailutils/validation.go index c1ce5a8e..746b2bd5 100644 --- a/apps/api/internal/email/validation.go +++ b/apps/api/internal/emailutils/validation.go @@ -1,4 +1,4 @@ -package email +package emailutils import "net/mail" diff --git a/apps/api/internal/parse/optional.go b/apps/api/internal/parse/optional.go index 5e8eadd1..66ce0932 100644 --- a/apps/api/internal/parse/optional.go +++ b/apps/api/internal/parse/optional.go @@ -1,13 +1,39 @@ package parse -import "encoding/json" +import ( + "bytes" + "encoding/json" + "reflect" -type Optional[T any] struct { - Value T - Present bool + "github.com/danielgtaylor/huma/v2" +) + +// https://huma.rocks/features/schema-customization/?h=unmar#field-schema +// +// OmittableNullable is a field which can be omitted from the input, +// set to `null`, or set to a value. Each state is tracked and can +// be checked for in handling code. +type OmittableNullable[T any] struct { + Sent bool + Null bool + Value T +} + +// UnmarshalJSON unmarshals this value from JSON input. +func (o *OmittableNullable[T]) UnmarshalJSON(b []byte) error { + if len(b) > 0 { + o.Sent = true + if bytes.Equal(b, []byte("null")) { + o.Null = true + // return nil + } + return json.Unmarshal(b, &o.Value) + } + return nil } -func (o *Optional[T]) UnmarshalJSON(data []byte) error { - o.Present = true - return json.Unmarshal(data, &o.Value) +// Schema returns a schema representing this value on the wire. +// It returns the schema of the contained type. +func (o OmittableNullable[T]) Schema(r huma.Registry) *huma.Schema { + return r.Schema(reflect.TypeOf(o.Value), true, "") } diff --git a/apps/api/internal/parse/parse.go b/apps/api/internal/parse/parse.go deleted file mode 100644 index 70d8fe55..00000000 --- a/apps/api/internal/parse/parse.go +++ /dev/null @@ -1,46 +0,0 @@ -package parse - -import ( - "fmt" - - "github.com/google/uuid" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -// Parses a string ptr to a UUID or nil if failed to parse -func ParseUUIDOrNil(s *string) *uuid.UUID { - if s == nil || *s == "" { - return nil - } - id, err := uuid.Parse(*s) - if err != nil { - return nil - } - return &id -} - -// Parses a string pointer to a pointer or nil -func ParseStrToPtr(s *string) *string { - if s == nil || *s == "" { - return nil - } - return s -} - -func ParseGetEventScopeType(s string) (sqlc.GetEventScopeType, error) { - enumType := sqlc.GetEventScopeType(s) - - // an empty string should default to published - if s == "" { - return sqlc.GetEventScopeTypePublished, nil - } - - // sqlc generates constants for each enum value. We check if the input matches one of the valid, known constants. - switch enumType { - case sqlc.GetEventScopeTypePublished, sqlc.GetEventScopeTypeScoped, sqlc.GetEventScopeTypeAll: - return enumType, nil - default: - // The input string is not a valid enum value. - return "", fmt.Errorf("'%s' is not a valid GetEventScopeType", s) - } -} diff --git a/apps/api/internal/ptr/bool.go b/apps/api/internal/ptr/bool.go deleted file mode 100644 index 959dd4fe..00000000 --- a/apps/api/internal/ptr/bool.go +++ /dev/null @@ -1,6 +0,0 @@ -package ptr - -// Takes a boolean and returns a pointer to that boolean -func BoolToPtr(b bool) *bool { - return &b -} diff --git a/apps/api/internal/ptr/int32.go b/apps/api/internal/ptr/int32.go deleted file mode 100644 index 0ec31804..00000000 --- a/apps/api/internal/ptr/int32.go +++ /dev/null @@ -1,5 +0,0 @@ -package ptr - -func Int32ToPtr(v int32) *int32 { - return &v -} diff --git a/apps/api/internal/ptr/uuid.go b/apps/api/internal/ptr/uuid.go deleted file mode 100644 index e82fa7a2..00000000 --- a/apps/api/internal/ptr/uuid.go +++ /dev/null @@ -1,8 +0,0 @@ -package ptr - -import "github.com/google/uuid" - -// Takes a UUID and returns a pointer to that UUID -func UUIDToPtr(id uuid.UUID) *uuid.UUID { - return &id -} diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go deleted file mode 100644 index 81801cb8..00000000 --- a/apps/api/internal/services/bat.go +++ /dev/null @@ -1,477 +0,0 @@ -package services - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/google/uuid" - "github.com/hibiken/asynq" - "github.com/jackc/pgx/v5" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/bat" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/tasks" - "golang.org/x/sync/errgroup" -) - -var ( - ErrListApplicationsFailure = errors.New("Failed to retrieve applications") - ErrMissingRatings = errors.New("Some applications are missing their review ratings") - ErrRunConflict = errors.New("Run already exists for this event") - ErrFailedToAddRun = errors.New("Failed to add run") - ErrFailedToDeleteRun = errors.New("Failed to delete run") - ErrFailedToUpdateRun = errors.New("Failed to update run") - ErrCouldNotGetEventInfo = errors.New("Could not retreive event info.") - ErrReviewsNotFinished = errors.New("Please make sure reviews are finished before calculating application decisions.") - ErrRunMismatch = errors.New("That bat run does not belong to this event.") - ErrRunStatusInvalid = errors.New("This run status is not valid for this action.") - ErrNoAcceptedApplicants = errors.New("No applicants marked as accepted.") - ErrFailedToCheckAppReviewsComplete = errors.New("Could not get determine if application reviews have finished.") - ErrReviewsNotComplete = errors.New("Please make sure reviews are finished before calculating application decisions.") - ErrCouldNotGetEmailInfo = errors.New("Could not get email info for applicant.") - ErrParseTemplateFilepathFailed = errors.New("Could not parse filepath for template.") - ErrFailedToSendDecisionEmails = errors.New("Failed to send decision emails") - ErrTestErr = errors.New("Err while testing") - ErrFailedToGetContactEmail = errors.New("Failed to get contact email") - ErrUserNotAttendee = errors.New("user is not an attendee") - ErrUserCheckedIn = errors.New("user already checked in") -) - -type BatService struct { - appRepo *repository.ApplicationRepository - eventRepo *repository.EventRepository - userRepo *repository.UserRepository - batRunsRepo *repository.BatRunsRepository - emailService *EmailService - txm *db.TransactionManager - taskQueue *asynq.Client - scheduler *asynq.Scheduler - logger zerolog.Logger -} - -func NewBatService(appRepo *repository.ApplicationRepository, eventRepo *repository.EventRepository, userRepo *repository.UserRepository, batRunsRepo *repository.BatRunsRepository, emailService *EmailService, txm *db.TransactionManager, taskQueue *asynq.Client, scheduler *asynq.Scheduler, logger zerolog.Logger) *BatService { - return &BatService{ - taskQueue: taskQueue, - scheduler: scheduler, - appRepo: appRepo, - eventRepo: eventRepo, - userRepo: userRepo, - batRunsRepo: batRunsRepo, - emailService: emailService, - txm: txm, - logger: logger.With().Str("service", "Bat Service").Str("component", "admissions").Logger(), - } -} - -func (s *BatService) ReleaseBatRunDecision(ctx context.Context, eventId, batRunId uuid.UUID) error { - // Retrieve Bat Run AND Event - g, egCtx := errgroup.WithContext(ctx) - - var event sqlc.Event - g.Go(func() error { - eventPtr, err := s.eventRepo.GetEventByID(egCtx, eventId) - if err != nil { - return err - } - - event = *eventPtr - return nil - }) - - var batRun sqlc.BatRun - g.Go(func() error { - run, err := s.batRunsRepo.GetRunById(egCtx, batRunId) - if err != nil { - return err - } - - batRun = run - return nil - }) - - if err := g.Wait(); err != nil { - return err - } - - if event.ID != batRun.EventID { - return ErrRunMismatch - } - - if batRun.Status.Valid != true || (batRun.Status.Valid == true && batRun.Status.BatRunStatus != sqlc.BatRunStatusCompleted) { - return ErrRunStatusInvalid - } - - if len(batRun.AcceptedApplicants) == 0 { - return ErrNoAcceptedApplicants - } - - err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { - txAppRepo := s.appRepo.NewTx(tx) - - err := txAppRepo.UpdateApplicationStatusByEventId(ctx, sqlc.ApplicationStatusAccepted, event.ID, batRun.AcceptedApplicants) - if err != nil { - return err - } - - return txAppRepo.UpdateApplicationStatusByEventId(ctx, sqlc.ApplicationStatusRejected, event.ID, batRun.RejectedApplicants) - }) - if err != nil { - return err - } - - err = s.SendDecisionEmails(ctx, batRun) - if err != nil { - return ErrFailedToSendDecisionEmails - } - - return nil -} - -func (s *BatService) SendDecisionEmails(ctx context.Context, batRun sqlc.BatRun) error { - cfg := config.Load() - accepetedEmailTemplatePath := cfg.EmailTemplateDirectory + "ApplicationAcceptedEmail.html" - rejectedEmailTemplatePath := cfg.EmailTemplateDirectory + "ApplicationRejectedEmail.html" - acceptedEmailSubject := "Congratulations on being accepted to hack in SwampHacks XI!" - rejectedEmailSubject := "Update on Your SwampHacks XI Application" - - for _, uuid := range batRun.AcceptedApplicants { - emailInfo, err := s.userRepo.GetUserEmailInfoById(ctx, uuid) - if err != nil { - return ErrCouldNotGetEmailInfo - } - - contactEmail, ok := emailInfo.ContactEmail.(string) - if !ok { - return ErrFailedToGetContactEmail - } - type emailTemplateData struct { - Name string - } - taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, acceptedEmailSubject, emailTemplateData{Name: emailInfo.Name}, accepetedEmailTemplatePath) - s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued acceptance email") - } - - for _, uuid := range batRun.RejectedApplicants { - emailInfo, err := s.userRepo.GetUserEmailInfoById(ctx, uuid) - if err != nil { - return ErrCouldNotGetEmailInfo - } - - contactEmail, ok := emailInfo.ContactEmail.(string) - if !ok { - return ErrFailedToGetContactEmail - } - type emailTemplateData struct { - Name string - } - taskInfo, err := s.emailService.QueueSendHtmlEmailTask(contactEmail, rejectedEmailSubject, emailTemplateData{emailInfo.Name}, rejectedEmailTemplatePath) - s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued rejection email") - } - - return nil -} - -func (s *BatService) AddRun(ctx context.Context, eventId uuid.UUID) (*sqlc.BatRun, error) { - - run, err := s.batRunsRepo.AddRun(ctx, eventId) - if err != nil && errors.Is(err, repository.ErrDuplicateRun) { - s.logger.Err(err).Msg("Could not insert result as it already exists.") - return nil, ErrRunConflict - } else if err != nil { - s.logger.Err(err).Msg("An unknown error was caught!") - return nil, ErrFailedToAddRun - } - - return run, nil -} - -func (s *BatService) GetRunsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetRunsByEventIdRow, error) { - return s.batRunsRepo.GetRunsByEventId(ctx, eventId) -} - -func (s *BatService) UpdateRunById(ctx context.Context, params sqlc.UpdateRunByIdParams) (*sqlc.BatRun, error) { - err := s.batRunsRepo.UpdateRunById(ctx, params) - if err != nil { - if errors.Is(err, repository.ErrRunNotFound) { - s.logger.Err(err).Msg(repository.ErrRunNotFound.Error()) - } else { - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return nil, ErrFailedToUpdateRun - } - - run, err := s.batRunsRepo.GetRunById(ctx, params.ID) - - return &run, err -} - -func (s *BatService) DeleteRunById(ctx context.Context, id uuid.UUID) error { - err := s.batRunsRepo.DeleteRunById(ctx, id) - if err != nil { - switch err { - case repository.ErrRunNotFound: - s.logger.Err(err).Msg(repository.ErrRunNotFound.Error()) - case repository.ErrNoRunsDeleted: - s.logger.Err(err).Msg(repository.ErrNoRunsDeleted.Error()) - case repository.ErrMultipleRunsDeleted: - s.logger.Err(err).Msg(repository.ErrMultipleRunsDeleted.Error()) - default: - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return ErrFailedToDeleteRun - } - - return err -} - -func (s *BatService) CheckApplicationReviewsComplete(ctx context.Context, eventId uuid.UUID) (bool, error) { - nonReviewedApplicantUUIDs, err := s.appRepo.GetNonReviewedApplications(ctx, eventId) - if err != nil { - return false, ErrFailedToCheckAppReviewsComplete - } - - return len(nonReviewedApplicantUUIDs) == 0, nil -} - -func (s *BatService) QueueCalculateAdmissionsTask(ctx context.Context, eventId uuid.UUID) (*asynq.TaskInfo, error) { - newRun, err := s.AddRun(ctx, eventId) - if err != nil { - return nil, ErrFailedToAddRun - } - - task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ - EventID: eventId, - BatRunID: newRun.ID, - }) - if err != nil { - s.logger.Err(err).Msg("Failed to create CalculateAdmissions task") - return nil, err - } - - info, err := s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue CalculateAdmissions task") - return nil, err - } - - return info, nil -} - -func (s *BatService) CalculateAdmissions(ctx context.Context, eventId, batRunId uuid.UUID) error { - s.logger.Debug().Str("RunID", batRunId.String()).Msg("") - - // check to make sure reviews are done, update state if true, return error if not - reviewStatus, err := s.CheckApplicationReviewsComplete(ctx, eventId) - if err != nil { - return ErrFailedToCheckAppReviewsComplete - } - if reviewStatus == false { - return ErrReviewsNotComplete - } - - engine, err := bat.NewBatEngine(0.6, 0.4) - if err != nil { - return err - } - - // Aggregate data necessary - applications, err := s.appRepo.ListAdmissionCandidatesByEvent(ctx, eventId) - if err != nil || len(applications) == 0 { - return ErrListApplicationsFailure - } - - admissionCandidates, err := s.mapToCandidates(engine, applications) - if err != nil { - return err - } - - teams, idvs := engine.GroupCandidates(admissionCandidates) - acceptedTeamMembers, remainder := engine.AcceptTeams(teams) - - idvs = append(idvs, remainder...) - acceptedIdvs, rejected := engine.AcceptIndividuals(idvs) - - accepted := append(acceptedTeamMembers, acceptedIdvs...) - - acceptedIDs := make([]uuid.UUID, 0, len(accepted)) - rejectedIDs := make([]uuid.UUID, 0, len(rejected)) - - for _, applicant := range accepted { - acceptedIDs = append(acceptedIDs, applicant.UserID) - } - for _, applicant := range rejected { - rejectedIDs = append(rejectedIDs, applicant.UserID) - } - - params := sqlc.UpdateRunByIdParams{ - // TODO: add UF/other/early/late info? - AcceptedApplicantsDoUpdate: true, - RejectedApplicantsDoUpdate: true, - StatusDoUpdate: true, - AcceptedApplicants: acceptedIDs, - RejectedApplicants: rejectedIDs, - Status: sqlc.NullBatRunStatus{ - BatRunStatus: sqlc.BatRunStatusCompleted, - Valid: true, - }, - ID: batRunId, - } - - err = s.batRunsRepo.UpdateRunById(ctx, params) - if err != nil { - return ErrFailedToUpdateRun - } - - s.logger.Info().Int("Teams Members Accepted", int(len(acceptedTeamMembers))).Int("Accepted", int(engine.Quota.TotalAccepted)).Int("Rejected", len(rejected)).Msg("Finished Algo") - - return nil -} - -// This could be moved into the engine instead, or some mapping function within the bat package. -func (s *BatService) mapToCandidates(engine *bat.BatEngine, applications []sqlc.ListAdmissionCandidatesByEventRow) ([]bat.AdmissionCandidate, error) { - var appAdmissionsData []bat.AdmissionCandidate - for _, app := range applications { - if app.ExperienceRating == nil || app.PassionRating == nil { - return []bat.AdmissionCandidate{}, ErrMissingRatings - } - - var admissionContext bat.AdmissionContext - if err := json.Unmarshal(app.Application, &admissionContext); err != nil { - s.logger.Debug().Bytes("App", app.Application).Msg("Application data") - return []bat.AdmissionCandidate{}, err - } - - var teamId uuid.UUID - if app.TeamID != nil { - teamId = *app.TeamID - } - - wScore, err := engine.CalculateWeightedScore(*app.PassionRating, *app.ExperienceRating) - if err != nil { - return []bat.AdmissionCandidate{}, err - } - appAdmissionsData = append(appAdmissionsData, bat.AdmissionCandidate{ - UserID: app.UserID, - TeamID: uuid.NullUUID{ - UUID: teamId, - Valid: app.TeamID != nil, - }, - WeightedScore: wScore, - SortKey: 0.0, - IsUFStudent: admissionContext.School == "University of Florida", - IsEarlyCareer: admissionContext.Year == "first_year" || admissionContext.Year == "second_year", - }) - } - - return appAdmissionsData, nil -} - -func (s *BatService) CheckInAttendee(ctx context.Context, eventId, userId uuid.UUID, RFID *string) error { - // Retrieve user with their current event role - role, err := s.eventRepo.GetEventRoleByIds(ctx, userId, eventId) - if err != nil { - return ErrUserNotFound - } - - if role.Role != sqlc.EventRoleTypeAttendee { - return ErrUserNotAttendee - } - - if role.CheckedInAt != nil { - return ErrUserCheckedIn - } - - now := time.Now() - // Update user role checked in AND rfid - return s.eventRepo.UpdateEventRoleByIds(ctx, sqlc.UpdateEventRoleByIdsParams{ - EventID: eventId, - UserID: userId, - - Role: sqlc.EventRoleTypeAttendee, - RoleDoUpdate: false, - - CheckedInAt: &now, - CheckedInAtDoUpdate: true, - - Rfid: RFID, - RfidDoUpdate: RFID != nil, - }) -} - -func (s *BatService) QueueScheduleWaitlistTransitionTask(ctx context.Context, eventId uuid.UUID) error { - - cfg := config.Load() - task, err := tasks.NewTaskScheduleTransitionWaitlist(tasks.ScheduleTransitionWaitlistPayload{ - EventID: eventId, - Period: cfg.AcceptFromWaitlistPeriod, - }) - if err != nil { - s.logger.Err(err).Msg("Failed to create ScheduleTransitionWaitlist task") - return err - } - - _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue ScheduleTransitionWaitlist task") - return err - } - s.logger.Info().Msg("Queued TransitionWaitlist task") - - return nil -} - -func (s *BatService) QueueShutdownWaitlistScheduler() error { - task, err := tasks.NewTaskShutdownScheduler() - if err != nil { - s.logger.Err(err).Msg("Failed to create ShutdownWaitlistScheduler task") - return err - } - - _, err = s.taskQueue.Enqueue(task, asynq.Queue("bat")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue ShutdownWaitlistScheduler task") - return err - } - s.logger.Info().Msg("Queued ShutdownWaitlistScheduler task") - - return nil -} - -func (s *BatService) SendWelcomeEmailToAttendees(ctx context.Context, eventId uuid.UUID) error { - attendees, err := s.eventRepo.GetAttendeeUserIdsByEventId(ctx, eventId) - if err != nil { - s.logger.Err(err).Msg("Could not get attendee user ids") - return err - } - - s.logger.Info().Msgf("Sending welcome emails to %v attendees", len(attendees)) - - for _, userId := range attendees { - contactInfo, err := s.userRepo.GetUserEmailInfoById(ctx, userId) - if err != nil { - s.logger.Err(err).Msgf("Could not get contact info for user with id %s", userId) - return err - } - contactEmail, ok := contactInfo.ContactEmail.(string) - if !ok { - s.logger.Err(err).Msgf("could got convert id %s", userId) - continue - } - if contactEmail == "" { - s.logger.Err(err).Msgf("empty contact email found for user with id %s", userId) - continue - } - - err = s.emailService.QueueWelcomeEmail(ctx, contactEmail, contactInfo.Name, userId) - if err != nil { - s.logger.Err(err).Msgf("Could not queue welcome email for user with id %s", userId) - return err - } - } - return nil -} diff --git a/apps/api/internal/services/discord.go b/apps/api/internal/services/discord.go deleted file mode 100644 index 70ee70dd..00000000 --- a/apps/api/internal/services/discord.go +++ /dev/null @@ -1,52 +0,0 @@ -package services - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrNoEventRole = errors.New("user has no event role") -) - -type DiscordService struct { - eventRepo *repository.EventRepository - logger zerolog.Logger -} - -func NewDiscordService( - eventRepo *repository.EventRepository, - logger zerolog.Logger, -) *DiscordService { - return &DiscordService{ - eventRepo: eventRepo, - logger: logger.With().Str("service", "DiscordService").Str("component", "discord").Logger(), - } -} - -func (s *DiscordService) GetUserEventRoleByDiscordIDAndEventId(ctx context.Context, discordID string, eventID uuid.UUID) (*sqlc.EventRoleType, error) { - eventRole, err := s.eventRepo.GetEventRoleByDiscordIDAndEventId(ctx, discordID, eventID) - if err != nil { - if err == repository.ErrEventRoleNotFound { - return nil, ErrNoEventRole - } - s.logger.Err(err).Msg("failed to get event role by discord ID and event ID") - return nil, err - } - - return &eventRole.Role, nil -} - -func (s *DiscordService) GetEventAttendeesWithDiscord(ctx context.Context, eventID uuid.UUID) (*[]sqlc.GetEventAttendeesWithDiscordRow, error) { - attendees, err := s.eventRepo.GetEventAttendeesWithDiscord(ctx, eventID) - if err != nil { - s.logger.Err(err).Msg("failed to get event attendees with discord") - return nil, err - } - return attendees, nil -} \ No newline at end of file diff --git a/apps/api/internal/services/email.go b/apps/api/internal/services/email.go deleted file mode 100644 index 462e8bab..00000000 --- a/apps/api/internal/services/email.go +++ /dev/null @@ -1,189 +0,0 @@ -package services - -import ( - "bytes" - "context" - "fmt" - "html/template" - - "github.com/google/uuid" - "github.com/hibiken/asynq" - "github.com/rs/zerolog" - "github.com/skip2/go-qrcode" - "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/email" - "github.com/swamphacks/core/apps/api/internal/storage" - "github.com/swamphacks/core/apps/api/internal/tasks" -) - -type EmailService struct { - logger zerolog.Logger - taskQueue *asynq.Client - SESClient *email.SESClient - storage storage.Storage -} - -func NewEmailService(taskQueue *asynq.Client, SESClient *email.SESClient, storage storage.Storage, logger zerolog.Logger) *EmailService { - return &EmailService{ - logger: logger.With().Str("service", "EmailService").Str("component", "email").Logger(), - taskQueue: taskQueue, - SESClient: SESClient, - storage: storage, - } -} - -func (s *EmailService) QueueConfirmationEmail(recipient string, name string) error { - cfg := config.Load() - - subject := "SwampHacks XI: we received your application!" - templateEmailFilepath := cfg.EmailTemplateDirectory + "ConfirmationEmail.html" - - type emailTemplateData struct { - Name string - } - _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) - - if err != nil { - s.logger.Err(err).Msg("Failed to send confirmation email to recipient") - return err - } - - return nil -} - -func (s *EmailService) QueueWelcomeEmail(ctx context.Context, recipient string, name string, userId uuid.UUID) error { - cfg := config.Load() - - qrString := fmt.Sprintf("IDENT::%s", userId) - qrPng, err := qrcode.Encode(qrString, qrcode.Medium, 256) - if err != nil { - s.logger.Err(err).Msg("Failed to generate QR code png") - return err - } - - contentType := "image/png" - if s.storage == nil { - s.logger.Err(err).Msg("A R2 client must be connected for this function to run") - return err - } - err = s.storage.Store(ctx, cfg.CoreBuckets.QRCodes, userId.String(), qrPng, &contentType) - if err != nil { - s.logger.Err(err).Msg("Failed to upload QR code to R2") - return err - } - - qrPngLink := fmt.Sprintf("%s/%s", cfg.CoreBuckets.QRCodesBaseUrl, userId.String()) - - subject := "SwampHacks XI – A welcome from our Organizers!" - templateEmailFilepath := cfg.EmailTemplateDirectory + "WelcomeEmail.html" - - type emailTemplateData struct { - Name string - QRPngLink string - } - _, err = s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name, QRPngLink: qrPngLink}, templateEmailFilepath) - - if err != nil { - s.logger.Err(err).Msgf("Failed to send welcome email to recipient with userId %s", userId.String()) - return err - } - - return nil -} - -func (s *EmailService) QueueWaitlistAcceptanceEmail(recipient string, name string) error { - cfg := config.Load() - - subject := "Congratulations! You're in – confirm in 72 hours to keep your spot in SwampHacks XI" - templateEmailFilepath := cfg.EmailTemplateDirectory + "WaitlistAcceptanceEmail.html" - - type emailTemplateData struct { - Name string - } - _, err := s.QueueSendHtmlEmailTask(recipient, subject, emailTemplateData{Name: name}, templateEmailFilepath) - - if err != nil { - s.logger.Err(err).Msg("Failed to send waitlist acceptance email to recipient") - return err - } - - return nil -} - -// SendHtmlEmail -// -// templateData: a struct holding the data which should replace {{}} tags inside of an html template. -// For example, if an email template uses the tag {{ .Name }}, then the templateData struct would look like -// type templateData struct { -// Name string -// } -func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateData interface{}, templateFilePath string) error { - var body bytes.Buffer - - template, err := template.ParseFiles(templateFilePath) - if err != nil { - s.logger.Err(err).Msg("Failed to parse email template for recipient") - } - - err = template.Execute(&body, templateData) - if err != nil { - s.logger.Err(err).Msg("Failed to inject template variables for recipient '%s'.") - } - - err = s.SESClient.SendHTMLEmail([]string{recipient}, "noreply@swamphacks.com", subject, body.String()) - if err != nil { - s.logger.Err(err).Msg("Failed to send html email to recipient") - return err - } - s.logger.Info().Str("Template", templateFilePath).Msg("Sent email") - - return nil -} - -func (s *EmailService) QueueSendHtmlEmailTask(to string, subject string, templateData interface{}, templateFilePath string) (*asynq.TaskInfo, error) { - if len(to) == 0 { - s.logger.Warn().Msgf("No recipient email found for email being sent from template '%s'", templateFilePath) - } - - task, err := tasks.NewTaskSendHtmlEmail(tasks.SendHtmlEmailPayload{ - To: to, - Subject: subject, - TemplateData: templateData, - TemplateFilePath: templateFilePath, - }) - - if err != nil { - s.logger.Err(err).Msg("Failed to create SendHtmlEmail task") - return nil, err - } - - taskInfo, err := s.taskQueue.Enqueue(task, asynq.Queue("email")) - s.logger.Info().Str("TaskID", taskInfo.ID).Str("Task Queue", taskInfo.Queue).Str("Task Type", taskInfo.Type).Msg("Queued SendHtmlEmail task!") - if err != nil { - s.logger.Err(err).Msg("Failed to queue SendHtmlEmail task") - return nil, err - } - - return taskInfo, nil -} - -func (s *EmailService) QueueSendTextEmail(to []string, subject string, body string) (*asynq.TaskInfo, error) { - task, err := tasks.NewTaskSendTextEmail(tasks.SendTextEmailPayload{ - To: to, - Subject: subject, - Body: body, - }) - - if err != nil { - s.logger.Err(err).Msg("Failed to create SendTextEmail task") - return nil, err - } - - info, err := s.taskQueue.Enqueue(task, asynq.Queue("email")) - if err != nil { - s.logger.Err(err).Msg("Failed to queue SendTextEmail task") - return nil, err - } - - return info, nil -} diff --git a/apps/api/internal/services/event_interest.go b/apps/api/internal/services/event_interest.go deleted file mode 100644 index fb34612a..00000000 --- a/apps/api/internal/services/event_interest.go +++ /dev/null @@ -1,47 +0,0 @@ -package services - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrEmailConflict = errors.New("email already exists in this mailing list") - ErrFailedToCreateSubmission = errors.New("failed to create event interest submission") -) - -type EventInterestService struct { - eventInterestRepo *repository.EventInterestRepository - logger zerolog.Logger -} - -func NewEventInterestService(eventInterestRepo *repository.EventInterestRepository, logger zerolog.Logger) *EventInterestService { - return &EventInterestService{ - eventInterestRepo: eventInterestRepo, - logger: logger.With().Str("service", "EventInterestService").Str("component", "event_interest").Logger(), - } -} - -func (s *EventInterestService) CreateInterestSubmission(ctx context.Context, eventID uuid.UUID, email string, source *string) (*sqlc.EventInterestSubmission, error) { - params := sqlc.AddEmailParams{ - EventID: eventID, - Email: email, - Source: source, - } - - result, err := s.eventInterestRepo.AddEmail(ctx, params) - if err != nil && errors.Is(err, repository.ErrDuplicateEmails) { - s.logger.Err(err).Msg("Could not insert email due to duplicate existing.") - return nil, ErrEmailConflict - } else if err != nil { - s.logger.Err(err).Msg("An unknown error was caught!") - return nil, ErrFailedToCreateSubmission - } - - return result, nil -} diff --git a/apps/api/internal/services/events.go b/apps/api/internal/services/events.go deleted file mode 100644 index c20c5918..00000000 --- a/apps/api/internal/services/events.go +++ /dev/null @@ -1,434 +0,0 @@ -package services - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "mime" - "mime/multipart" - "path/filepath" - "strings" - "time" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/config" - ctxu "github.com/swamphacks/core/apps/api/internal/ctxutils" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/storage" - "golang.org/x/sync/errgroup" -) - -var ( - ErrFailedToCreateEvent = errors.New("failed to create event") - ErrFailedToGetEvent = errors.New("failed to get event") - ErrFailedToUpdateEvent = errors.New("failed to update event") - ErrFailedToDeleteEvent = errors.New("failed to delete event") - ErrFailedToParseUUID = errors.New("failed to parse uuid") - ErrMissingFields = errors.New("missing fields") - ErrMissingPerms = errors.New("missing perms") - ErrFailedToUploadBanner = errors.New("failed to upload banner") - ErrUnexpectedFileType = errors.New("did not expect this file type") - - ErrFailedToSubmitApplication = errors.New("failed to submit application") - ErrGetEventOverview = errors.New("failed to aggregate event stats") -) - -type EventService struct { - eventRepo *repository.EventRepository - userRepo *repository.UserRepository - storage storage.Storage - buckets *config.CoreBuckets - logger zerolog.Logger -} - -func NewEventService(eventRepo *repository.EventRepository, userRepo *repository.UserRepository, storage storage.Storage, buckets *config.CoreBuckets, logger zerolog.Logger) *EventService { - return &EventService{ - eventRepo: eventRepo, - userRepo: userRepo, - storage: storage, - buckets: buckets, - logger: logger.With().Str("service", "EventService").Str("component", "events").Logger(), - } -} - -func (s *EventService) CreateEvent(ctx context.Context, params sqlc.CreateEventParams) (*sqlc.Event, error) { - event, err := s.eventRepo.CreateEvent(ctx, params) - if err != nil { - if errors.Is(err, repository.ErrEventNotFound) { - s.logger.Err(err).Msg(repository.ErrEventNotFound.Error()) - } else { - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return nil, ErrFailedToCreateEvent - } - - return event, nil -} - -func (s *EventService) GetEventByID(ctx context.Context, id uuid.UUID) (*sqlc.Event, error) { - event, err := s.eventRepo.GetEventByID(ctx, id) - if err != nil { - if errors.Is(err, repository.ErrEventNotFound) { - s.logger.Err(err).Msg(repository.ErrEventNotFound.Error()) - } else { - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return nil, ErrFailedToGetEvent - } - - return event, nil -} - -func (s *EventService) UpdateEventById(ctx context.Context, params sqlc.UpdateEventByIdParams) (*sqlc.Event, error) { - err := s.eventRepo.UpdateEventById(ctx, params) - if err != nil { - if errors.Is(err, repository.ErrEventNotFound) { - s.logger.Err(err).Msg(repository.ErrEventNotFound.Error()) - } else { - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return nil, ErrFailedToUpdateEvent - } - - event, err := s.eventRepo.GetEventByID(ctx, params.ID) - - return event, err -} - -func (s *EventService) DeleteEventById(ctx context.Context, id uuid.UUID) error { - err := s.eventRepo.DeleteEventById(ctx, id) - if err != nil { - switch err { - case repository.ErrEventNotFound: - s.logger.Err(err).Msg(repository.ErrEventNotFound.Error()) - case repository.ErrNoEventsDeleted: - s.logger.Err(err).Msg(repository.ErrEventNotFound.Error()) - case repository.ErrMultipleEventsDeleted: - s.logger.Err(err).Msg(repository.ErrMultipleEventsDeleted.Error()) - default: - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return ErrFailedToDeleteEvent - } - - return err -} - -func (s *EventService) GetEvents(ctx context.Context, scope sqlc.GetEventScopeType) (*[]sqlc.GetEventsWithUserInfoRow, error) { - isSuperuser := ctxu.IsSuperuser(ctx) - userId := ctxu.GetUserIdFromCtx(ctx) - - // Non-superusers can't get all unpublished events - if !isSuperuser && scope == "all" { - return nil, ErrMissingPerms - } - - return s.eventRepo.GetEventsWithRoles(ctx, userId, scope) - -} - -func (s *EventService) GetEventRoleByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (*sqlc.EventRole, error) { - eventRole, err := s.eventRepo.GetEventRoleByIds(ctx, userId, eventId) - if err != nil { - if errors.Is(err, repository.ErrEventRoleNotFound) { - s.logger.Err(err).Msg(repository.ErrEventRoleNotFound.Error()) - } else { - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return nil, err - } - - return eventRole, err -} - -func (s *EventService) GetEventStaffUsers(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetEventStaffRow, error) { - return s.eventRepo.GetEventStaff(ctx, eventId) -} - -func (s *EventService) GetEventUsers(ctx context.Context, eventID uuid.UUID) (*[]sqlc.GetEventUsersRow, error) { - return s.eventRepo.GetEventUsers(ctx, eventID) -} - -func (s *EventService) AssignEventRole( - ctx context.Context, - userId *uuid.UUID, - email *string, - eventId uuid.UUID, - role sqlc.EventRoleType, -) error { - if userId == nil && email == nil { - return errors.New("must provide either userId or email") - } - - var selectedUser *sqlc.AuthUser - var err error - - if userId != nil { - selectedUser, err = s.userRepo.GetByID(ctx, *userId) - // Do not return if user not found, the query needs to fallback to other optiosn - if err != nil && !errors.Is(err, repository.ErrUserNotFound) { - s.logger.Err(err).Msg("Something went wrong getting by id") - return err - } - } - - if selectedUser == nil && email != nil { - selectedUser, err = s.userRepo.GetByEmail(ctx, *email) - if err != nil { - s.logger.Err(err).Msg("Something went wrong getting by email") - return err - } - } - - // Just a double safety check (should usually be caught by queries above) - if selectedUser == nil { - s.logger.Warn().Msg(("User not found from email OR id")) - return repository.ErrUserNotFound - } - - // Now assign the event role - err = s.eventRepo.AssignRole(ctx, sqlc.AssignRoleParams{ - EventID: eventId, - UserID: selectedUser.ID, - Role: role, - }) - if err != nil { - return err - } - - return nil -} - -func (s *EventService) RevokeEventRole(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { - return s.eventRepo.RevokeRole(ctx, userId, eventId) -} - -func (s *EventService) UpdateEventRole(ctx context.Context, userId uuid.UUID, eventId uuid.UUID, role sqlc.EventRoleType) error { - return s.eventRepo.UpdateRole(ctx, userId, eventId, role) -} - -// If you need to set any of these fields to nil, then you need to find a different function. -// If you pass in nil, it will mark that field as do not update. -func (s *EventService) UpdateEventRoleByIds(ctx context.Context, userId, eventId uuid.UUID, role *sqlc.EventRoleType, checkedInAt *time.Time, RFID *string) error { - if role == nil && checkedInAt == nil && RFID == nil { - return errors.New("no fields provided to update") - } - - return s.eventRepo.UpdateEventRoleByIds(ctx, sqlc.UpdateEventRoleByIdsParams{ - EventID: eventId, - UserID: userId, - Role: *role, - RoleDoUpdate: role != nil, - - CheckedInAt: checkedInAt, - CheckedInAtDoUpdate: checkedInAt != nil, - - Rfid: RFID, - RfidDoUpdate: RFID != nil, - }) -} - -func (s *EventService) IsApplicationsOpen(ctx context.Context, eventId uuid.UUID) (bool, error) { - event, err := s.GetEventByID(ctx, eventId) - if err != nil { - s.logger.Err(err).Msg("ApplicationOpen check error: " + err.Error()) - return false, err - } - - if !*event.IsPublished { - return false, nil - } - - now := time.Now() - open := now.After(event.ApplicationOpen) && now.Before(event.ApplicationClose) - return open, nil -} - -func (s *EventService) UploadBanner(ctx context.Context, eventId uuid.UUID, banner multipart.File, header *multipart.FileHeader) (*string, error) { - bannerFileBuffer := bytes.NewBuffer(nil) - - fileName := header.Filename - fileExt := strings.ToLower(filepath.Ext(fileName)) - - s.logger.Info().Str("Filetype", fileExt).Msg("The file type") - - switch fileExt { - case ".jpg", ".png", ".jpeg": - // Do nothing - default: - return nil, ErrUnexpectedFileType - } - - fileType := mime.TypeByExtension(fileExt) - - if fileType == "" { - return nil, ErrFailedToUploadBanner - } - - if _, err := io.Copy(bannerFileBuffer, banner); err != nil { - return nil, ErrFailedToUploadBanner - } - - uploadKey := fmt.Sprintf("%s/banner%s", eventId, fileExt) - - err := s.storage.Store(ctx, s.buckets.EventAssets, uploadKey, bannerFileBuffer.Bytes(), &fileType) - if err != nil { - return nil, ErrFailedToUploadBanner - } - - // Reconstrust URL with cache buster - url := fmt.Sprintf("%s/%s?t=%d", s.buckets.EventAssetsBaseUrl, uploadKey, time.Now().Unix()) - - err = s.eventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{ - ID: eventId, - BannerDoUpdate: true, - Banner: &url, - }) - if err != nil { - return nil, ErrFailedToUpdateEvent - } - - return &url, nil - -} - -func (s *EventService) DeleteBanner(ctx context.Context, eventId uuid.UUID) error { - // For now its a soft delete, not actually deleting banner is easiest, just set to null - return s.eventRepo.UpdateEventById(ctx, sqlc.UpdateEventByIdParams{ - ID: eventId, - BannerDoUpdate: true, - Banner: nil, - }) -} - -type SubmissionTimesStatistics struct { - Day time.Time `json:"day" format:"date-time"` - Count int64 `json:"count"` -} - -type EventOverview struct { - EventDetails sqlc.Event `json:"event_details"` - ApplicationStatusStatistics sqlc.GetApplicationStatusSplitRow `json:"application_status_stats"` - SubmissionTimesStatistics []SubmissionTimesStatistics `json:"application_submission_stats"` -} - -func (s *EventService) GetEventOverview(ctx context.Context, eventId uuid.UUID) (*EventOverview, error) { - g, ctx := errgroup.WithContext(ctx) - - var eventDetails *sqlc.Event - var statusStats sqlc.GetApplicationStatusSplitRow - var submissionTimesStats []SubmissionTimesStatistics - - g.Go(func() error { - var err error - eventDetails, err = s.GetEventByID(ctx, eventId) - return err - }) - - g.Go(func() error { - var err error - statusStats, err = s.eventRepo.GetApplicationStatuses(ctx, eventId) - return err - }) - - g.Go(func() error { - var err error - temp, err := s.eventRepo.GetSubmissionTimes(ctx, eventId) - - for _, v := range temp { - submissionTimesStats = append(submissionTimesStats, SubmissionTimesStatistics{ - Count: v.Count, - Day: v.Day.Time, - }) - } - - fmt.Println(submissionTimesStats) - return err - }) - - if err := g.Wait(); err != nil { - s.logger.Err(err).Msg("Something went wrong while getting event overview stats") - return nil, ErrGetEventOverview - } - - return &EventOverview{ - EventDetails: *eventDetails, - ApplicationStatusStatistics: statusStats, - SubmissionTimesStatistics: submissionTimesStats, - }, nil -} - -type UserInfoForEvent struct { - UserID uuid.UUID `json:"user_id"` - Name string `json:"name"` - Email *string `json:"email"` - PlatformRole sqlc.AuthUserRole `json:"platform_role"` - EventRole sqlc.EventRoleType `json:"event_role"` - Image *string `json:"image"` - CheckedInAt *time.Time `json:"checked_in_at"` -} - -func (s *EventService) GetUserByRFID(ctx context.Context, eventId uuid.UUID, rfid string) (*sqlc.AuthUser, error) { - user, err := s.eventRepo.GetUserByRFID(ctx, eventId, rfid) - if err != nil { - if errors.Is(err, repository.ErrEventRoleNotFound) { - s.logger.Err(err).Msg("User not found with RFID") - return nil, repository.ErrUserNotFound - } - s.logger.Err(err).Msg("Failed to get user by RFID") - return nil, err - } - return user, nil -} - -func (s *EventService) GetUserInfoForEvent(ctx context.Context, userId, eventId uuid.UUID) (*UserInfoForEvent, error) { - g, ctx := errgroup.WithContext(ctx) - - var user *sqlc.AuthUser - var role *sqlc.EventRole - - g.Go(func() error { - var err error - user, err = s.userRepo.GetByID(ctx, userId) - return err - }) - - //TODO: This may not run very nicely if the user doesn't have an associated event_role - // Oh well... for now! - g.Go(func() error { - var err error - role, err = s.eventRepo.GetEventRoleByIds(ctx, userId, eventId) - return err - }) - - if err := g.Wait(); err != nil { - s.logger.Err(err).Msg("Something went wrong while getting event overview stats") - return nil, err - } - - result := &UserInfoForEvent{ - UserID: user.ID, - Name: user.Name, - Email: user.Email, - PlatformRole: user.Role, - EventRole: role.Role, - Image: user.Image, - CheckedInAt: role.CheckedInAt, - } - - return result, nil -} - -func (s *EventService) GetCheckedInStatusByIds(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) (bool, error) { - result, err := s.eventRepo.GetCheckedInStatusByUserIdAndEventId(ctx, userId, eventId) - if err != nil { - s.logger.Err(err).Msg("Failed to get user by ids") - return false, err - } - return result, nil - -} diff --git a/apps/api/internal/services/user.go b/apps/api/internal/services/user.go deleted file mode 100644 index 97de3ac3..00000000 --- a/apps/api/internal/services/user.go +++ /dev/null @@ -1,81 +0,0 @@ -package services - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrUserNotFound = errors.New("user not found") - ErrFailedToUpdateUser = errors.New("failed to update user") - ErrFailedToGetUser = errors.New("failed to get user") -) - -type UserService struct { - userRepo *repository.UserRepository - logger zerolog.Logger -} - -func NewUserService(userRepo *repository.UserRepository, logger zerolog.Logger) *UserService { - return &UserService{ - userRepo: userRepo, - logger: logger.With().Str("service", "UserService").Str("component", "user").Logger(), - } -} - -// GetUser retrieves a user by their ID -func (s *UserService) GetUser(ctx context.Context, userId uuid.UUID) (*sqlc.AuthUser, error) { - user, err := s.userRepo.GetByID(ctx, userId) - if err != nil { - if err == repository.ErrUserNotFound { - s.logger.Err(err).Msg(repository.ErrUserNotFound.Error()) - return nil, ErrUserNotFound - } else { - s.logger.Err(err).Msg("failed to get user by ID") - return nil, ErrFailedToGetUser - } - } - - return user, nil -} - -func (s *UserService) UpdateUser(ctx context.Context, userId uuid.UUID, params sqlc.UpdateUserParams) error { - // Set the user ID in the params - params.ID = userId - - err := s.userRepo.UpdateUser(ctx, params) - if err != nil { - if err == repository.ErrUserNotFound { - s.logger.Err(err).Msg(repository.ErrUserNotFound.Error()) - return ErrUserNotFound - } else { - s.logger.Err(err).Msg("failed to update user") - return ErrFailedToUpdateUser - } - } - - return nil -} - -func (s *UserService) CompleteOnboarding(ctx context.Context, userId uuid.UUID, name, email string) error { - params := sqlc.UpdateUserParams{ - ID: userId, - NameDoUpdate: true, - Name: name, - PreferredEmailDoUpdate: true, - PreferredEmail: &email, - OnboardedDoUpdate: true, - Onboarded: true, - } - - return s.UpdateUser(ctx, userId, params) -} - -func (s *UserService) GetAllUsers(ctx context.Context, search *string, limit, offset int32) ([]sqlc.AuthUser, error) { - return s.userRepo.GetAllUsers(ctx, search, limit, offset) -} diff --git a/apps/api/internal/tasks/bat.go b/apps/api/internal/tasks/bat.go index 28164ea4..9a24e71c 100644 --- a/apps/api/internal/tasks/bat.go +++ b/apps/api/internal/tasks/bat.go @@ -15,17 +15,14 @@ const ( ) type CalculateAdmissionsPayload struct { - EventID uuid.UUID BatRunID uuid.UUID } type ScheduleTransitionWaitlistPayload struct { - EventID uuid.UUID - Period string + Period string } type TransitionWaitlistPayload struct { - EventID uuid.UUID AcceptFromWaitlistCount uint32 MaxAcceptedApplications uint32 } diff --git a/apps/api/internal/workers/bat.go b/apps/api/internal/workers/bat.go index 1c4108f5..d84ac8a6 100644 --- a/apps/api/internal/workers/bat.go +++ b/apps/api/internal/workers/bat.go @@ -9,8 +9,10 @@ import ( "github.com/hibiken/asynq" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/config" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" - "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/database/sqlc" + "github.com/swamphacks/core/apps/api/internal/domains/application" + "github.com/swamphacks/core/apps/api/internal/domains/bat" + "github.com/swamphacks/core/apps/api/internal/domains/hackathon" "github.com/swamphacks/core/apps/api/internal/tasks" ) @@ -26,21 +28,27 @@ var ( // other decision heuristics. It operates asynchronously to ensure // fair, consistent, and scalable admissions handling. type BATWorker struct { - batService *services.BatService - applicationService *services.ApplicationService - eventService *services.EventService + batService *bat.BatService + applicationService *application.ApplicationService + hackathonService *hackathon.HackathonService scheduler *asynq.Scheduler taskQueue *asynq.Client + config *config.Config logger zerolog.Logger } -func NewBATWorker(batService *services.BatService, applicationService *services.ApplicationService, eventService *services.EventService, scheduler *asynq.Scheduler, taskQueue *asynq.Client, logger zerolog.Logger) *BATWorker { +func NewBATWorker( + batService *bat.BatService, applicationService *application.ApplicationService, + hackathonService *hackathon.HackathonService, scheduler *asynq.Scheduler, + taskQueue *asynq.Client, config *config.Config, logger zerolog.Logger, +) *BATWorker { return &BATWorker{ batService: batService, applicationService: applicationService, - eventService: eventService, - logger: logger.With().Str("worker", "BATWorker").Str("component", "BAT").Logger(), + hackathonService: hackathonService, + logger: logger.With().Str("worker", "BATWorker").Logger(), scheduler: scheduler, + config: config, taskQueue: taskQueue, } } @@ -52,20 +60,17 @@ func (w *BATWorker) HandleCalculateAdmissionsTask(ctx context.Context, t *asynq. return err } - err := w.batService.CalculateAdmissions(ctx, p.EventID, p.BatRunID) + err := w.batService.CalculateAdmissions(ctx, p.BatRunID) // On error, mark run as failed. // To be fair, this should be handled more granulary. Like for example, // what if the original run was never created? Just food for thought for now. if err != nil { w.logger.Err(err).Msg("Something went wrong calculating admissions.") - _, _ = w.batService.UpdateRunById(ctx, sqlc.UpdateRunByIdParams{ + _, _ = w.batService.UpdateRunById(ctx, sqlc.UpdateBatRunByIdParams{ ID: p.BatRunID, StatusDoUpdate: true, - Status: sqlc.NullBatRunStatus{ - BatRunStatus: sqlc.BatRunStatusFailed, - Valid: true, - }, + Status: sqlc.BatRunStatusFailed, }) return err @@ -80,23 +85,20 @@ func (w *BATWorker) HandleScheduleTransitionWaitlistTask(ctx context.Context, t return err } - // Get event start date, error if past the start of the event. - event, err := w.eventService.GetEventByID(ctx, payload.EventID) + hackathon, err := w.hackathonService.GetHackathon(ctx) if err != nil { - w.logger.Err(err).Msg(err.Error()) + w.logger.Err(err).Msg("Failed to get hackathon in HandleScheduleTransitionWaitlistTask") return err } currentTime := time.Now() - if currentTime.After(event.StartTime) { + if currentTime.After(hackathon.StartTime) { w.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") return ErrEventAlreadyStarted } - cfg := config.Load() task, err := tasks.NewTaskTransitionWaitlist(tasks.TransitionWaitlistPayload{ - EventID: payload.EventID, - AcceptFromWaitlistCount: cfg.AcceptFromWaitlistCount, - MaxAcceptedApplications: cfg.MaxAcceptedApplications, + AcceptFromWaitlistCount: w.config.AcceptFromWaitlistCount, + MaxAcceptedApplications: w.config.MaxAcceptedApplications, }) // The scheduler will make its first run after the period cycles once. So we queue our task immediately as well. @@ -119,7 +121,7 @@ func (w *BATWorker) HandleTransitionWaitlistTask(ctx context.Context, t *asynq.T return err } - err := w.applicationService.TransitionWaitlistedApplications(ctx, payload.EventID, payload.AcceptFromWaitlistCount, payload.MaxAcceptedApplications) + err := w.applicationService.TransitionWaitlistedApplications(ctx, payload.AcceptFromWaitlistCount, payload.MaxAcceptedApplications) if err != nil { w.logger.Err(err) return nil diff --git a/apps/api/internal/workers/email.go b/apps/api/internal/workers/email.go index 77a6478f..026e74cb 100644 --- a/apps/api/internal/workers/email.go +++ b/apps/api/internal/workers/email.go @@ -7,19 +7,19 @@ import ( "github.com/hibiken/asynq" "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/domains/email" "github.com/swamphacks/core/apps/api/internal/tasks" ) type EmailWorker struct { - emailService *services.EmailService + emailService *email.EmailService logger zerolog.Logger } -func NewEmailWorker(emailService *services.EmailService, logger zerolog.Logger) *EmailWorker { +func NewEmailWorker(emailService *email.EmailService, logger zerolog.Logger) *EmailWorker { return &EmailWorker{ emailService: emailService, - logger: logger.With().Str("worker", "EmailWorker").Str("component", "email").Logger(), + logger: logger.With().Str("worker", "EmailWorker").Logger(), } } diff --git a/apps/api/sqlc.yml b/apps/api/sqlc.yml index f35ea367..583f7e59 100644 --- a/apps/api/sqlc.yml +++ b/apps/api/sqlc.yml @@ -1,12 +1,12 @@ version: "2" sql: - engine: postgresql - queries: "internal/db/queries/" - schema: "internal/db/migrations/" + queries: "internal/database/queries/" + schema: "internal/database/migrations/" gen: go: package: "sqlc" - out: "internal/db/sqlc" + out: "internal/database/sqlc" emit_json_tags: true emit_prepared_queries: false emit_interface: false @@ -19,6 +19,9 @@ sql: go_type: import: "github.com/google/uuid" type: "UUID" + - db_type: "uuid" + nullable: true + go_type: "*github.com/google/uuid.UUID" - db_type: "timestamptz" go_type: type: "time.Time" @@ -26,6 +29,10 @@ sql: nullable: true go_type: type: "*time.Time" - - db_type: uuid + - db_type: "timestamp with time zone" nullable: true - go_type: "*github.com/google/uuid.UUID" + go_type: + type: "*time.Time" + - db_type: "timestamp with time zone" + go_type: + type: "time.Time" diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 2c3f904b..69b5976a 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -62,7 +62,7 @@ nav: - Installation & Setup: api/installation.md - Project Structure: api/structure.md - Authentication & Roles: api/auth.md - - Database Schema (Neon): api/database.md + - Database Schema: api/database.md - Migrations: api/migrations.md - Database Testing: api/db_testing.md - OpenAPI: api/openapi.md diff --git a/apps/docs/src/api/auth.md b/apps/docs/src/api/auth.md index 0d1ba316..54172022 100644 --- a/apps/docs/src/api/auth.md +++ b/apps/docs/src/api/auth.md @@ -54,27 +54,12 @@ Every request to a protected route goes through `RequireAuth` middleware: | `Name` | string | Display name | | `Onboarded` | bool | Whether onboarding is complete | | `Image` | `*string` | Profile image URL | -| `Role` | `AuthUserRole` | Platform role (`user` or `superuser`) | +| `Role` | `UserRole` | `admin`, `staff`, `attendee`, `applicant`, `visitor` | | `EmailConsent` | bool | Whether the user opted into emails | --- -## Platform Roles - -Two platform-level roles are defined in `auth_user_role`: - -| Role | Description | -|---|---| -| `user` | Default role for all registered users | -| `superuser` | Full access; bypasses all role checks | - -Platform roles are enforced by `RequirePlatformRole(roles)` middleware. Superusers bypass this check unconditionally. - ---- - -## Event Roles - -Users can have a role within a specific event, stored in `event_roles`: +## User Roles | Role | Description | |---|---| @@ -82,8 +67,9 @@ Users can have a role within a specific event, stored in `event_roles`: | `staff` | Event operations (check-in, review applications, manage redeemables) | | `attendee` | Accepted attendee | | `applicant` | Has submitted an application | +| `visitor` | Has made an account, but never submitted an application | -Event roles are enforced by `RequireEventRole(roles)` middleware, which fetches the user's role for the event from the URL path. Superusers bypass event role checks. +R oles are enforced by `RequireEventRole(roles)` middleware, which fetches the user's role for the event from the URL path. Superusers bypass event role checks. --- @@ -104,5 +90,5 @@ Routes under `/mobile` require this header. The key is configured via the `MOBIL | Method | Path | Auth | Description | |---|---|---|---| | `GET` | `/auth/callback` | None | OAuth2 callback | -| `GET` | `/auth/me` | Session | Get current user | +| `GET` | `/users/me` | Session | Get current user | | `POST` | `/auth/logout` | Session | Invalidate session | diff --git a/apps/docs/src/api/database.md b/apps/docs/src/api/database.md index 28275aa2..cf25058d 100644 --- a/apps/docs/src/api/database.md +++ b/apps/docs/src/api/database.md @@ -1,14 +1,14 @@ # Database Schema -The API uses **PostgreSQL 17**. Auth-related tables live in the `auth` schema; everything else is in the default `public` schema. +The API uses **PostgreSQL 17**. Everything is in the default `public` schema. All tables include `created_at` and `updated_at` timestamps. `updated_at` is maintained automatically by a `update_modified_column()` trigger. --- -## Auth Schema +## Public Schema -### `auth.users` +### `users` Core user accounts. One record per registered user. @@ -22,18 +22,21 @@ Core user accounts. One record per registered user. | `email_consent` | BOOLEAN | Marketing email opt-in | | `onboarded` | BOOLEAN | Whether onboarding is complete | | `image` | TEXT | Profile image URL | -| `role` | `auth_user_role` | `user` or `superuser` | +| `rfid` | TEXT | RFID string | +| `role` | `UserRole` | `admin`, `staff`, `attendee`, `applicant`, `visitor` | +| `role_assigned_at` | TIMESTAMPTZ | | +| `checked_in_at` | TIMESTAMPTZ | | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | -### `auth.accounts` +### `accounts` OAuth provider associations. A user can have multiple providers (currently only Discord). | Column | Type | Notes | |---|---|---| | `id` | UUID PK | | -| `user_id` | UUID FK → `auth.users` | | +| `user_id` | UUID FK → `users` | | | `provider_id` | TEXT | e.g., `discord` | | `account_id` | TEXT | Provider's user ID | | `access_token` | TEXT | | @@ -44,14 +47,14 @@ OAuth provider associations. A user can have multiple providers (currently only Unique constraint on `(provider_id, account_id)`. -### `auth.sessions` +### `sessions` Active user sessions. Sessions expire and use rolling expiration. | Column | Type | Notes | |---|---|---| | `id` | UUID PK | Stored in `sh_session_id` cookie | -| `user_id` | UUID FK → `auth.users` | | +| `user_id` | UUID FK → `users` | | | `expires_at` | TIMESTAMPTZ | Extended on use after 24h | | `ip_address` | TEXT | | | `user_agent` | TEXT | | @@ -60,171 +63,4 @@ Active user sessions. Sessions expire and use rolling expiration. --- -## Public Schema - -### `events` - -Hackathon events. Drives the application and attendee lifecycle. - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `name` | TEXT | | -| `description` | TEXT | | -| `location` | TEXT | | -| `location_url` | TEXT | | -| `max_attendees` | INT | Optional cap | -| `application_open` | TIMESTAMPTZ | Applications open | -| `application_close` | TIMESTAMPTZ | Applications close | -| `rsvp_deadline` | TIMESTAMPTZ | | -| `decision_release` | TIMESTAMPTZ | When decisions are released to applicants | -| `start_time` | TIMESTAMPTZ | | -| `end_time` | TIMESTAMPTZ | | -| `website_url` | TEXT | | -| `banner_url` | TEXT | R2 object key for the banner image | -| `is_published` | BOOLEAN | `false` = draft (only staff+ can see) | -| `application_review_started` | BOOLEAN | Locks in reviewer assignments | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -### `event_roles` - -Maps users to their role within a specific event. - -| Column | Type | Notes | -|---|---|---| -| `user_id` | UUID FK → `auth.users` | | -| `event_id` | UUID FK → `events` | | -| `role` | `event_role_type` | `admin`, `staff`, `attendee`, `applicant` | -| `assigned_at` | TIMESTAMPTZ | | - -Primary key: `(user_id, event_id)`. - -### `applications` - -One application per user per event. Stores the form data as JSONB. - -| Column | Type | Notes | -|---|---|---| -| `user_id` | UUID FK → `auth.users` | | -| `event_id` | UUID FK → `events` | | -| `status` | `application_status` | See statuses below | -| `application` | JSONB | Form field data | -| `experience_rating` | INTEGER | Reviewer score (1–5) | -| `passion_rating` | INTEGER | Reviewer score (1–5) | -| `assigned_reviewer_id` | UUID FK → `auth.users` | Nullable | -| `submitted_by` | UUID | User ID at time of submission | -| `waitlisted_at` | TIMESTAMPTZ | When the applicant was waitlisted | -| `saved_at` | TIMESTAMPTZ | Last draft save | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -Primary key: `(user_id, event_id)`. - -**`application_status` enum:** - -| Value | Description | -|---|---| -| `started` | Draft — created but not submitted | -| `submitted` | Submitted, awaiting review | -| `under_review` | Assigned to a reviewer | -| `accepted` | Accepted by BAT run | -| `rejected` | Rejected by BAT run | -| `waitlisted` | On waitlist | -| `withdrawn` | Withdrawn by applicant | - -### `bat_runs` - -Records of BAT (Balanced Admissions Thresher) execution results. - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `event_id` | UUID FK → `events` | | -| `accepted_applicants` | UUID[] | Array of user IDs | -| `rejected_applicants` | UUID[] | Array of user IDs | -| `status` | `bat_run_status` | `running`, `completed`, `failed` | -| `created_at` | TIMESTAMPTZ | | -| `completed_at` | TIMESTAMPTZ | Nullable | - -### `teams` - -Teams within an event. - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `name` | TEXT | | -| `owner_id` | UUID FK → `auth.users` | Nullable (SET NULL on delete) | -| `event_id` | UUID FK → `events` | | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -### `team_members` - -Many-to-many membership join table. - -| Column | Type | Notes | -|---|---|---| -| `user_id` | UUID FK → `auth.users` | | -| `team_id` | UUID FK → `teams` | | -| `joined_at` | TIMESTAMPTZ | | - -Primary key: `(user_id, team_id)`. - -### `team_join_requests` - -Requests from users to join a team. - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `team_id` | UUID FK → `teams` | | -| `user_id` | UUID FK → `auth.users` | | -| `request_message` | TEXT | Optional message | -| `status` | `join_request_status` | `PENDING`, `APPROVED`, `REJECTED` | -| `processed_by_user_id` | UUID FK → `auth.users` | Nullable | -| `processed_at` | TIMESTAMPTZ | Nullable | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -Unique partial index: one `PENDING` request per `(team_id, user_id)`. - -### `event_interest_submissions` - -Mailing list for event interest (pre-registration). - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `event_id` | UUID FK → `events` | | -| `email` | TEXT | | -| `created_at` | TIMESTAMPTZ | | - -### `redeemables` - -Prize or reward items associated with an event. - -| Column | Type | Notes | -|---|---|---| -| `id` | UUID PK | | -| `event_id` | UUID FK → `events` | | -| `name` | VARCHAR(255) | | -| `amount` | INT | Total available (≥ 0) | -| `max_user_amount` | INT | Per-user limit (≥ 1) | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -### `user_redemptions` - -Tracks how many times a user has redeemed a specific redeemable. - -| Column | Type | Notes | -|---|---|---| -| `user_id` | UUID FK → `auth.users` | | -| `redeemable_id` | UUID FK → `redeemables` | | -| `amount` | INT | Times redeemed (≥ 0) | -| `created_at` | TIMESTAMPTZ | | -| `updated_at` | TIMESTAMPTZ | | - -Primary key: `(user_id, redeemable_id)`. +TODO: Document other tables \ No newline at end of file diff --git a/apps/docs/src/api/index.md b/apps/docs/src/api/index.md index bad4ef3f..f22df694 100644 --- a/apps/docs/src/api/index.md +++ b/apps/docs/src/api/index.md @@ -6,7 +6,7 @@ The SwampHacks API is the central backend for the platform. It handles authentic | Component | Technology | |---|---| -| Language | Go 1.24 | +| Language | Go 1.26+ | | Router | [chi](https://github.com/go-chi/chi) | | Database | PostgreSQL 17 (via [pgx](https://github.com/jackc/pgx)) | | Query layer | [sqlc](https://sqlc.dev) (generated, type-safe) | @@ -14,7 +14,7 @@ The SwampHacks API is the central backend for the platform. It handles authentic | Object storage | Cloudflare R2 (S3-compatible) | | Email | AWS SES | | Auth | Discord OAuth2 + session cookies | -| API docs | [Scalar](https://scalar.com) (served at `/docs`) | +| API docs | [Huma Framework](https://huma.rocks/) (served at `/docs`) | ## Architecture @@ -46,7 +46,7 @@ Both workers share the same codebase and configuration as the API but are starte |---|---| | Auth | Discord OAuth2 login, session management | | Users | Profiles, onboarding, email consent | -| Events | Hackathon event lifecycle, banners, scopes | +| Hackathon | Hackathon event lifecycle, banners, scopes | | Applications | Submission, review assignment, BAT decisions, waitlist | | Teams | Creation, join requests, membership | | Redeemables | Prize tracking and redemption | @@ -56,6 +56,7 @@ Both workers share the same codebase and configuration as the API but are starte ## API Documentation -Interactive API documentation is available at `/docs` when the server is running. The raw OpenAPI spec is at `apps/api/docs/swagger.yaml`. +Interactive API documentation is available at `/docs` when the server is running. The raw OpenAPI spec is at `apps/api/docs/openapi.json`. +Because our OpenAPI documentation is code-first, we have to manually the raw openapi.json file whenever we update the documentation inside our code. To do this, `cd` into `/apps/api/` and run the `download-openapi.sh` script inside the terminal. -An external hosted version is available at [core.apidocumentation.com](https://core.apidocumentation.com/guide/swamphacks-core-api). + diff --git a/apps/docs/src/api/installation.md b/apps/docs/src/api/installation.md index 5ddde1be..5642a6a5 100644 --- a/apps/docs/src/api/installation.md +++ b/apps/docs/src/api/installation.md @@ -17,19 +17,17 @@ If you are writing migrations, modifying database queries, or updating the OpenA go version ``` -**2. Install goose and sqlc:** +**2. Install Huma:** ```bash -go install github.com/pressly/goose/v3/cmd/goose@latest -go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest +go get github.com/danielgtaylor/huma/v2 ``` -**3. Install swag** (custom fork, `v2` branch): +**2. Install goose and sqlc:** ```bash -git clone -b v2 https://github.com/hieunguyent12/swag.git -cd swag -go install ./cmd/swag +go install github.com/pressly/goose/v3/cmd/goose@latest +go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest ``` All three are invoked via `make` targets in `apps/api/`. See [Migrations](migrations.md) and [OpenAPI](openapi.md) for usage. @@ -79,11 +77,11 @@ make backend ## Migrations -Database migrations run automatically on startup. To run them manually from the host: +Database migrations must be run manually on the development database. ```bash cd apps/api -go run ./cmd/migrate +make migrate-up ``` > The `DATABASE_URL_MIGRATION` variable points to `localhost:5432` (host network) rather than the Docker internal hostname, so migrations work when run outside the container. diff --git a/apps/docs/src/api/migrations.md b/apps/docs/src/api/migrations.md index 620a52d2..3c4df61c 100644 --- a/apps/docs/src/api/migrations.md +++ b/apps/docs/src/api/migrations.md @@ -30,10 +30,10 @@ Generate a new migration file (requires [goose installed](installation.md#api-de ```bash cd apps/api -goose -dir internal/db/migrations create sql +goose -dir internal/database/migrations create sql ``` -This creates a timestamped file in `internal/db/migrations/`. Write your `Up` and `Down` SQL, then commit the file. +This creates a timestamped file in `internal/database/migrations/`. Write your `Up` and `Down` SQL, then commit the file. !!! warning Never modify an existing migration file. If you need to alter a table, create a new migration. @@ -58,41 +58,13 @@ cd apps/api make migrate-down ``` -## Migration history - -| File | Description | -|---|---| -| `20250512145328_auth_init` | Auth schema: `users`, `accounts`, `sessions` tables | -| `20250608015747_remove_session_token_and_update_users` | Removed token column, updated users schema | -| `20250608194039_add_roles_to_auth_users` | Added `role` column + `auth_user_role` enum | -| `20250619161938_event_schema` | `events` and `event_roles` tables | -| `20250621222955_create_applications` | `applications` table + `application_status` enum | -| `20250627064521_create_mailing_list` | `event_interest_submissions` table | -| `20250825013123_remove_resume_url_column` | Removed `resume_url` from applications | -| `20250825033231_update_application_trigger` | Updated application `updated_at` trigger | -| `20250905210705_add_preferred_email_column` | Added `preferred_email` to users | -| `20250908055118_add_email_consent_column` | Added `email_consent` to users | -| `20250917044049_add_event_banner` | Added `banner_url` to events | -| `20251002000347_add_get_event_scope_type` | Added event scope enum for query filtering | -| `20251022032904_submitted_by_column_applications` | Added `submitted_by` to applications | -| `20251101211753_teams_table` | `teams` and `team_members` tables | -| `20251106160823_saved_at_trigger` | Added `saved_at` trigger to applications | -| `20251111212010_invitations_and_join_requests` | `team_invitations` and `team_join_requests` tables | -| `20251121165836_add_app_review_columns` | Added reviewer rating columns + `application_review_started` | -| `20251208221807_add_application_waitlist_time_column` | Added `waitlisted_at` to applications | -| `20251215225937_add_bat_runs_schema` | `bat_runs` table + `bat_run_status` enum | -| `20251216200020_add_application_review_finished` | Added review finished flag | -| `20251217065809_remove_application_review_finished_column` | Removed review finished flag | -| `20260116002956_checked_in_time` | Added check-in timestamp to event roles | -| `20260119015108_create_redeemables_tables` | `redeemables` and `user_redemptions` tables | - -## SQLc regeneration - -After modifying a migration or a query file in `internal/db/queries/`, regenerate the Go code: +## SQLC regeneration + +After modifying a migration or a query file in `internal/database/queries/`, regenerate the Go code: ```bash cd apps/api make generate ``` -This updates `internal/db/sqlc/` — never edit that directory manually. See `sqlc.yml` for the full codegen configuration. +This updates `internal/database/sqlc/` — never edit that directory manually. See `sqlc.yml` for the full codegen configuration. diff --git a/apps/docs/src/api/openapi.md b/apps/docs/src/api/openapi.md index ba009669..11529d34 100644 --- a/apps/docs/src/api/openapi.md +++ b/apps/docs/src/api/openapi.md @@ -1,151 +1,3 @@ # OpenAPI -The API uses [swag](https://github.com/swaggo/swag) to generate an OpenAPI 3.1 spec from annotations written directly in Go source files. The generated output lives in `apps/api/docs/` and is served at `/docs` by the running API server via [Scalar](https://scalar.com). - -A hosted version of the spec is also available at [core.apidocumentation.com](https://core.apidocumentation.com/guide/swamphacks-core-api). - ---- - -## Installation - -The project uses a custom fork of swag on its `v2` branch. Install it from source: - -```bash -git clone -b v2 https://github.com/hieunguyent12/swag.git -cd swag -go install ./cmd/swag -``` - -> Go must be installed. See [Installation & Setup](installation.md) for instructions. - -Verify: - -```bash -swag --version -``` - ---- - -## Generating the spec - -From `apps/api/`: - -```bash -make openapi-generate -``` - -This runs: - -``` -swag init --dir cmd/api,internal/api/handlers --parseDependency --requiredByDefault -v3.1 -``` - -swag scans `cmd/api/main.go` (for top-level API metadata) and all handler files in `internal/api/handlers/` for route annotations. It writes three output files: - -| File | Description | -|---|---| -| `docs/swagger.yaml` | OpenAPI 3.1 spec (YAML) | -| `docs/swagger.json` | OpenAPI 3.1 spec (JSON) | -| `docs/docs.go` | Embedded Go file for serving the spec at runtime | - -Run this any time you add or change handler annotations. - -## Formatting annotations - -swag can also normalise the annotation comments in-place: - -```bash -make openapi-format -``` - -This runs `swag fmt` over the handler files. Run it before committing annotation changes to keep formatting consistent. - ---- - -## How annotations work - -### API-level metadata - -Top-level metadata is declared in `cmd/api/main.go` above the `main` function: - -```go -// @title SwampHacks Test API -// @version 1.0 -// @description This is SwampHacks' OpenAPI documentation. -``` - -### Route annotations - -Each exported handler method that corresponds to a route gets a block of annotations directly above its signature. swag reads these to build the spec. - -**Example** (`internal/api/handlers/auth.go`): - -```go -// GetMe -// -// @Summary Get Current User -// @Description Get the currently authenticated user's information. -// @Tags Authentication -// @Produce json -// @Param sh_session cookie string true "The authenticated session token/id" -// @Success 200 {object} middleware.UserContext -// @Failure 401 {object} response.ErrorResponse "Unauthenticated" -// @Failure 500 {object} response.ErrorResponse -// @Router /auth/me [get] -func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) { -``` - -**Example** (`internal/api/handlers/events.go`): - -```go -// Create a new event -// -// @Summary Create a new event -// @Description Create a new event with the provided details -// @Tags Event -// @Accept json -// @Produce json -// @Param request body CreateEventFields true "Event creation data" -// @Success 201 {object} sqlc.Event "Event created" -// @Failure 400 {object} response.ErrorResponse "Bad request" -// @Router /events [post] -func (h *EventHandler) CreateEvent(w http.ResponseWriter, r *http.Request) { -``` - -### Common annotation fields - -| Annotation | Description | -|---|---| -| `@Summary` | Short one-line description shown in the spec | -| `@Description` | Longer description | -| `@Tags` | Groups the route under a tag in the UI | -| `@Accept` | Request content type (e.g. `json`) | -| `@Produce` | Response content type (e.g. `json`) | -| `@Param` | Parameter definition — see format below | -| `@Success` | Success response with status code and body type | -| `@Failure` | Error response with status code and body type | -| `@Router` | Path and HTTP method — **required** | - -### `@Param` format - -``` -@Param "" -``` - -`` is one of: `query`, `path`, `body`, `header`, `cookie`. - -```go -@Param eventId path string true "Event UUID" -@Param request body CreateEventFields true "Request body" -@Param limit query int false "Page size" -``` - -### Response body types - -Pass a Go struct to `{object}` and swag will reflect its fields into the spec. Types from other packages work as long as `--parseDependency` is set (it is, via `make openapi-generate`): - -```go -@Success 200 {object} sqlc.Event -@Success 201 {object} middleware.UserContext -@Failure 400 {object} response.ErrorResponse -``` +The API uses [Huma](https://huma.rocks/) to generate an OpenAPI 3.1 spec from code written directly in Go source files. The generated output is served at `/docs` when running the API server. \ No newline at end of file diff --git a/apps/docs/src/api/structure.md b/apps/docs/src/api/structure.md index e1484775..4258e9c0 100644 --- a/apps/docs/src/api/structure.md +++ b/apps/docs/src/api/structure.md @@ -14,17 +14,15 @@ apps/api/ ├── internal/ │ ├── api/ │ │ ├── api.go # Router setup, route registration -│ │ ├── handlers/ # HTTP handlers (one file per domain) +│ | ├── cookie/ # Session cookie helpers │ │ ├── middleware/ │ │ │ ├── auth.go # Session auth, platform roles -│ │ │ └── events.go # Event-scoped role middleware -│ │ └── response/ # JSON response helpers -│ ├── bat/ # BAT engine logic +│ │ ├── response/ # JSON response helpers +│ | └── web/ # HTTP path/query param helpers │ ├── config/ │ │ └── config.go # Env-driven configuration structs -│ ├── cookie/ # Session cookie helpers │ ├── ctxutils/ # Context extraction helpers -│ ├── db/ +│ ├── database/ │ │ ├── connection.go # pgxpool setup │ │ ├── errors.go # DB error helpers (unique, not found) │ │ ├── transaction.go # Transaction manager @@ -32,7 +30,7 @@ apps/api/ │ │ ├── queries/ # Raw SQL query files (sqlc input) │ │ ├── repository/ # Data access objects │ │ └── sqlc/ # Generated Go code (do not edit) -│ ├── email/ +│ ├── emailutils/ │ │ ├── ses.go # AWS SES client │ │ ├── templates/ # HTML email templates │ │ └── validation.go # Email address validation @@ -41,12 +39,11 @@ apps/api/ │ │ └── discord.go # Discord OAuth2 exchange + user info │ ├── parse/ # Generic optional type, safe parsers │ ├── ptr/ # Pointer helpers -│ ├── services/ # Business logic (one file per domain) +│ ├── domains/ # Business logic (one package per domain) │ ├── storage/ │ │ ├── r2.go # Cloudflare R2 client │ │ └── presignable_storage.go │ ├── tasks/ # Asynq task definitions -│ ├── web/ # HTTP path/query param helpers │ └── workers/ # Worker process implementations ├── docs/ # Generated Swagger/OpenAPI spec ├── Dockerfile # Production multi-stage build @@ -67,17 +64,17 @@ Handlers live in `internal/api/handlers/` with one file per domain (e.g., `event - Calling the appropriate service method - Writing JSON responses -### Services +### Domains -Services live in `internal/services/` with one file per domain. They contain all business logic and coordinate between repositories. Services receive repositories via constructor injection and use the transaction manager for operations that require atomicity. +Domains live in `internal/domains/` with one package per domain. They contain all business logic and coordinate between repositories. Domains receive repositories via constructor injection and use the transaction manager for operations that require atomicity. ### Repositories -Repositories live in `internal/db/repository/` and provide a type-safe data access layer over the sqlc-generated queries. Each repository wraps a `*sqlc.Queries` and exposes domain-specific methods. Repositories support transactions via `NewTx(tx)`. +Repositories live in `internal/database/repository/` and provide a type-safe data access layer over the sqlc-generated queries. Each repository wraps a `*sqlc.Queries` and exposes domain-specific methods. Repositories support transactions via `NewTx(tx)`. ### Generated Code -`internal/db/sqlc/` is fully generated by sqlc — never edit it directly. To regenerate after changing a query or migration: +`internal/database/sqlc/` is fully generated by sqlc — never edit it directly. To regenerate after changing a query or migration: ```bash cd apps/api diff --git a/apps/docs/src/getting-started.md b/apps/docs/src/getting-started.md index 8a78771e..a727931e 100644 --- a/apps/docs/src/getting-started.md +++ b/apps/docs/src/getting-started.md @@ -22,7 +22,7 @@ docker compose version ### Node.js (via nvm) -The web app requires **Node 22.16**. Use [nvm](https://github.com/nvm-sh/nvm) to manage versions. +The web app requires **Node 22.16+**. Use [nvm](https://github.com/nvm-sh/nvm) to manage versions. You can install [nvm here](https://github.com/nvm-sh/nvm) (MacOS / Linux / WSL). diff --git a/apps/docs/src/index.md b/apps/docs/src/index.md index 7eb34e7e..ac0d1e3b 100644 --- a/apps/docs/src/index.md +++ b/apps/docs/src/index.md @@ -12,11 +12,11 @@ Whether you are a new organizer joining the technical team or looking to underst Our platform is built with a modern, scalable stack designed for rapid development and high availability during the event: -* **Frontend:** React + Vite +* **Frontend:** React & TypeScript + Vite * **Backend:** Go -* **Database:** Neon DB (Serverless Postgres) +* **Database:** TODO (previously Neon DB) * **Secrets Management:** Infisical -* **Infrastructure:** Docker & DigitalOcean +* **Infrastructure:** Docker & DigitalOcean, Github Actions * **Community:** Discord Bot (Custom) --- diff --git a/apps/docs/src/repo-structure.md b/apps/docs/src/repo-structure.md index e69de29b..30404ce4 100644 --- a/apps/docs/src/repo-structure.md +++ b/apps/docs/src/repo-structure.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/apps/docs/src/workflow.md b/apps/docs/src/workflow.md index e69de29b..30404ce4 100644 --- a/apps/docs/src/workflow.md +++ b/apps/docs/src/workflow.md @@ -0,0 +1 @@ +TODO \ No newline at end of file From 87c49a1dc4d802ad42e9660b4350ddcf31746d9d Mon Sep 17 00:00:00 2001 From: hieu Date: Tue, 31 Mar 2026 13:51:18 -0400 Subject: [PATCH 07/11] Update api prod dockerfile and openapi.json --- apps/api/Dockerfile | 2 +- apps/api/docs/openapi.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 3fd23019..be895e96 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder RUN apk add --no-cache build-base ca-certificates diff --git a/apps/api/docs/openapi.json b/apps/api/docs/openapi.json index c346a7c1..d93f41e8 100644 --- a/apps/api/docs/openapi.json +++ b/apps/api/docs/openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"Application":{"additionalProperties":false,"properties":{"application":{"contentEncoding":"base64","type":"string"},"assigned_reviewer_id":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"experience_rating":{"format":"int32","type":["integer","null"]},"hackathon_iteration":{"type":"string"},"passion_rating":{"format":"int32","type":["integer","null"]},"saved_at":{"format":"date-time","type":"string"},"status":{"$ref":"#/components/schemas/NullApplicationStatus"},"submitted_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"},"waitlist_join_time":{"format":"date-time","type":["string","null"]}},"required":["user_id","status","application","created_at","saved_at","updated_at","submitted_at","experience_rating","passion_rating","assigned_reviewer_id","waitlist_join_time","hackathon_iteration"],"type":"object"},"ApplicationStatistics":{"additionalProperties":false,"properties":{"age_stats":{"$ref":"#/components/schemas/GetApplicationAgeSplitRow"},"gender_stats":{"$ref":"#/components/schemas/GetApplicationGenderSplitRow"},"major_stats":{"items":{"$ref":"#/components/schemas/GetApplicationMajorSplitRow"},"type":["array","null"]},"race_stats":{"items":{"$ref":"#/components/schemas/GetApplicationRaceSplitRow"},"type":["array","null"]},"school_stats":{"items":{"$ref":"#/components/schemas/GetApplicationSchoolSplitRow"},"type":["array","null"]},"status_stats":{"$ref":"#/components/schemas/GetApplicationStatusSplitRow"}},"required":["gender_stats","age_stats","race_stats","major_stats","school_stats","status_stats"],"type":"object"},"AssignRoleBatchRequest":{"additionalProperties":false,"properties":{"assignments":{"items":{"$ref":"#/components/schemas/AssignRoleRequest"},"type":["array","null"]}},"required":["assignments"],"type":"object"},"AssignRoleRequest":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"role":{"type":"string"},"user_id":{"type":["string","null"]}},"required":["email","user_id","role"],"type":"object"},"AssignedApplication":{"additionalProperties":false,"properties":{"applicantId":{"type":"string"},"status":{"type":"string"}},"required":["applicantId","status"],"type":"object"},"CheckInRequest":{"additionalProperties":false,"properties":{"rfid":{"type":["string","null"]},"user_id":{"type":"string"}},"required":["user_id","rfid"],"type":"object"},"CreateJoinRequest":{"additionalProperties":false,"properties":{"message":{"type":["string","null"]}},"required":["message"],"type":"object"},"CreateRedeemableRequest":{"additionalProperties":false,"properties":{"amount":{"format":"int64","minimum":1,"type":"integer"},"max_user_amount":{"format":"int64","type":"integer"},"name":{"minLength":1,"type":"string"}},"required":["name","amount","max_user_amount"],"type":"object"},"CreateTeamRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"GetApplicationAgeSplitRow":{"additionalProperties":false,"properties":{"age_18":{"format":"int64","type":"integer"},"age_19":{"format":"int64","type":"integer"},"age_20":{"format":"int64","type":"integer"},"age_21":{"format":"int64","type":"integer"},"age_22":{"format":"int64","type":"integer"},"age_23_plus":{"format":"int64","type":"integer"},"underage":{"format":"int64","type":"integer"}},"required":["underage","age_18","age_19","age_20","age_21","age_22","age_23_plus"],"type":"object"},"GetApplicationGenderSplitRow":{"additionalProperties":false,"properties":{"female":{"format":"int64","type":"integer"},"male":{"format":"int64","type":"integer"},"non_binary":{"format":"int64","type":"integer"},"other":{"format":"int64","type":"integer"}},"required":["male","female","non_binary","other"],"type":"object"},"GetApplicationMajorSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"major":{"type":"string"}},"required":["major","count"],"type":"object"},"GetApplicationRaceSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"race_group":{"type":"string"}},"required":["race_group","count"],"type":"object"},"GetApplicationSchoolSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"school":{"type":"string"}},"required":["school","count"],"type":"object"},"GetApplicationStatusSplitRow":{"additionalProperties":false,"properties":{"accepted":{"format":"int64","type":"integer"},"rejected":{"format":"int64","type":"integer"},"started":{"format":"int64","type":"integer"},"submitted":{"format":"int64","type":"integer"},"under_review":{"format":"int64","type":"integer"},"waitlisted":{"format":"int64","type":"integer"},"withdrawn":{"format":"int64","type":"integer"}},"required":["started","submitted","under_review","accepted","rejected","waitlisted","withdrawn"],"type":"object"},"GetAttendeesWithDiscordRow":{"additionalProperties":false,"properties":{"discord_id":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["discord_id","user_id","name","email"],"type":"object"},"GetRedeemablesRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"total_redeemed":{},"total_stock":{"format":"int32","type":"integer"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","total_stock","max_user_amount","created_at","updated_at","total_redeemed"],"type":"object"},"Hackathon":{"additionalProperties":false,"properties":{"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"application_review_started":{"type":"boolean"},"banner":{"type":["string","null"]},"created_at":{"format":"date-time","type":["string","null"]},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"is_published":{"type":["boolean","null"]},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"onerow_id":{"type":"boolean"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":["string","null"]},"website_url":{"type":["string","null"]}},"required":["name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","website_url","is_published","created_at","updated_at","banner","application_review_started","onerow_id"],"type":"object"},"ListJoinRequestsByTeamAndStatusWithUserRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_email":{"type":["string","null"]},"user_id":{"type":"string"},"user_image":{"type":["string","null"]},"user_name":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at","user_email","user_name","user_image"],"type":"object"},"MemberWithUserInfo":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"image":{"type":["string","null"]},"joined_at":{"format":"date-time","type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["user_id","email","image","name","joined_at"],"type":"object"},"NullApplicationStatus":{"additionalProperties":false,"properties":{"application_status":{"type":"string"},"valid":{"type":"boolean"}},"required":["application_status","valid"],"type":"object"},"OnboardingRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferred_email":{"type":"string"}},"required":["name","preferred_email"],"type":"object"},"Redeemable":{"additionalProperties":false,"properties":{"amount":{"format":"int32","type":"integer"},"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","amount","max_user_amount","created_at","updated_at"],"type":"object"},"ReviewRatings":{"additionalProperties":false,"properties":{"experience_rating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"},"passion_rating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"}},"required":["passion_rating","experience_rating"],"type":"object"},"ReviewerAssignment":{"additionalProperties":false,"properties":{"amount":{"format":"int64","type":["integer","null"]},"userId":{"type":"string"}},"required":["userId","amount"],"type":"object"},"SubmitInterestEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"source":{"type":["string","null"]}},"required":["email","source"],"type":"object"},"Team":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":["string","null"]},"id":{"type":"string"},"name":{"type":"string"},"owner_id":{"type":"string"},"updated_at":{"format":"date-time","type":["string","null"]}},"required":["id","name","owner_id","created_at","updated_at"],"type":"object"},"TeamJoinRequest":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at"],"type":"object"},"TeamWithMembers":{"additionalProperties":false,"properties":{"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/MemberWithUserInfo"},"type":["array","null"]},"name":{"type":"string"},"owner_id":{"type":"string"}},"required":["id","owner_id","name","members"],"type":"object"},"UpdateEmailConsentRequest":{"additionalProperties":false,"properties":{"email_consent":{"type":"boolean"}},"required":["email_consent"],"type":"object"},"UpdateHackathonRequest":{"additionalProperties":false,"properties":{"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"is_published":{"type":"boolean"},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"website_url":{"type":["string","null"]}},"required":["name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","website_url","is_published"],"type":"object"},"UpdateRedeemableRequest":{"additionalProperties":false,"properties":{"max_user_amount":{"format":"int64","type":"integer"},"name":{"type":"string"},"total_stock":{"format":"int64","type":"integer"}},"type":"object"},"UpdateRedemptionRequest":{"additionalProperties":false,"properties":{"new_amount":{"format":"int64","type":"integer"}},"type":"object"},"UpdateUserRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferred_email":{"type":"string"}},"required":["name","preferred_email"],"type":"object"},"User":{"additionalProperties":false,"properties":{"checked_in_at":{"format":"date-time","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"email":{"type":["string","null"]},"email_consent":{"type":"boolean"},"email_verified":{"type":"boolean"},"id":{"type":"string"},"image":{"type":["string","null"]},"name":{"type":"string"},"onboarded":{"type":"boolean"},"preferred_email":{"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"type":"string"},"role_assigned_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","email","email_verified","onboarded","image","created_at","updated_at","preferred_email","email_consent","checked_in_at","rfid","role_assigned_at","role"],"type":"object"},"UserContext":{"additionalProperties":false,"properties":{"checkedInAt":{"format":"date-time","type":["string","null"]},"email":{"examples":["user@example.com"],"type":["string","null"]},"emailConsent":{"examples":[false],"type":"boolean"},"image":{"examples":["https://cdn.example.com/avatar.png"],"type":["string","null"]},"name":{"examples":["Jane Doe"],"type":"string"},"onboarded":{"examples":[true],"type":"boolean"},"preferredEmail":{"examples":["user.alt@example.com"],"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"enum":["admin","staff","attendee","applicant","visitor"],"type":"string"},"userId":{"examples":["550e8400-e29b-41d4-a716-446655440000"],"format":"uuid","type":"string"}},"required":["userId","email","preferredEmail","name","onboarded","image","role","emailConsent","rfid","checkedInAt"],"type":"object"}}},"info":{"title":"SwampHacks API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/application":{"get":{"description":"Get the application of the current user","operationId":"get-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application","tags":["Application"]}},"/application/accept-acceptance":{"patch":{"description":"Accept an acceptance after being accepted. Sets event role to attendee, from applicant.","operationId":"accept-application-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Application Acceptance","tags":["Application"]}},"/application/assigned":{"get":{"description":"Returns assigned applications and their review progress for the authenticated reviewer","operationId":"get-assigned-applications","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AssignedApplication"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Assigned Applications","tags":["Application"]}},"/application/calculate-admissions":{"post":{"description":"Queues an admission calculation task to the BAT worker","operationId":"calculate-admissions-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Admissions Calculation Request","tags":["Application"]}},"/application/join-waitlist":{"patch":{"description":"Adds a waitlist join time to application. Sets status to waitlisted","operationId":"join-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Join Waitlist","tags":["Application"]}},"/application/release-decisions/{runId}":{"post":{"description":"Releases decisions that were calculated by the worker from a specific run id","operationId":"release-decisions","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"runId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Release Decisions","tags":["Application"]}},"/application/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.","operationId":"get-download-resume-url","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume Download URL","tags":["Application"]}},"/application/review/assign":{"post":{"description":"Assigns applications to reviewers for the application review process.","operationId":"assign-application-reviewers","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ReviewerAssignment"},"type":["array","null"]}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Application Reviewers","tags":["Application"]}},"/application/review/reset":{"post":{"description":"Resets all application reviews, clearing any existing reviewer assignments.","operationId":"reset-application-reviews","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reset Application Reviews","tags":["Application"]}},"/application/review/{applicantId}":{"post":{"description":"Handles ratings submissions from staff during the application review process","operationId":"submit-application-review","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRatings"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application Review","tags":["Application"]}},"/application/review/{applicantId}/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.","operationId":"get-resume","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume URL (for review process)","tags":["Application"]}},"/application/save":{"post":{"description":"Save user's progress on the application. File/Upload fields are not saved (eg. resumes).","operationId":"save-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{}}}},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Save Application","tags":["Application"]}},"/application/stats":{"get":{"description":"Aggregates applications by race, gender, age, majors, and schools","operationId":"get-application-statistics","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationStatistics"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application Statistics","tags":["Application"]}},"/application/submit":{"post":{"description":"Submit the application","operationId":"submit-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application","tags":["Application"]}},"/application/transition-waitlisted-applications":{"patch":{"description":"Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.","operationId":"transition-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Transition Waitlisted Applications","tags":["Application"]}},"/application/withdraw-acceptance":{"patch":{"description":"Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.","operationId":"withdraw-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Acceptance","tags":["Application"]}},"/application/withdraw-attendance":{"patch":{"description":"Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.","operationId":"withdraw-attendance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Attendance","tags":["Application"]}},"/auth/callback":{"get":{"description":"Handles the OAuth provider callback, validates state and nonce, and sets the session cookie.","operationId":"oauth-callback","parameters":[{"description":"OAuth authorization code","explode":false,"in":"query","name":"code","required":true,"schema":{"description":"OAuth authorization code","type":"string"}},{"description":"Base64 encoded OAuth state","explode":false,"in":"query","name":"state","required":true,"schema":{"description":"Base64 encoded OAuth state","type":"string"}},{"description":"Auth nonce cookie for CSRF protection","in":"cookie","name":"sh_auth_nonce","required":true,"schema":{"description":"Auth nonce cookie for CSRF protection","type":"string"}},{"description":"Client user agent","in":"header","name":"User-Agent","schema":{"description":"Client user agent","type":"string"}}],"responses":{"204":{"description":"No Content","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"schema":{"type":"string"}}}},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"501":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Implemented"}},"summary":"OAuth Callback","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Logs out the authenticated user by invalidating their session","operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Logout","tags":["Auth"]}},"/hackathon":{"get":{"description":"Returns information of the hackathon","operationId":"get-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon","tags":["Hackathon"]},"patch":{"description":"Updates the information of the hackathon","operationId":"update-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHackathonRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Hackathon","tags":["Hackathon"]}},"/hackathon/attendees/count":{"get":{"description":"Returns the number of users who is attending the hackathon","operationId":"get-hackathon-attendees-count","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"format":"int64","type":"integer"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees Count","tags":["Hackathon"]}},"/hackathon/attendees/discord":{"get":{"description":"Returns all users with a discord account that is also attending the hackathon","operationId":"get-hackathon-attendees-with-discord","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetAttendeesWithDiscordRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees with Discord","tags":["Hackathon"]}},"/hackathon/attendees/userids":{"get":{"description":"Returns all users ids of users who are attending the hackathon","operationId":"get-hackathon-attendees-userids","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees User Ids","tags":["Hackathon"]}},"/hackathon/checkin":{"get":{"description":"Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.","operationId":"check-in","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckInRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Check In User","tags":["Hackathon"]}},"/hackathon/interest":{"post":{"description":"Submits an email to interest/mailing list for the hackathon","operationId":"submit-interest-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitInterestEmailRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Interest Email","tags":["Hackathon"]}},"/hackathon/staff":{"get":{"description":"Returns the users who are part of the current staff of the hackathon","operationId":"get-hackathon-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Staff","tags":["Hackathon"]}},"/redeemables":{"get":{"description":"Returns a list of all redeemable items","operationId":"get-redeemables","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetRedeemablesRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Redeemables","tags":["Redeemables"]},"post":{"description":"Creates a new redeemable item","operationId":"create-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRedeemableRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Redeemable"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}":{"delete":{"description":"Deletes a redeemable by id","operationId":"delete-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Redeemable","tags":["Redeemables"]},"patch":{"description":"Update specific fields (name, stock, max per user) of a redeemable","operationId":"update-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedeemableRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}/users/{userId}":{"patch":{"description":"Updates a redemption created by the user.","operationId":"update-redemption","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedemptionRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redemption","tags":["Redeemables"]},"post":{"description":"Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item","operationId":"redeem-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Redeem Redeemable","tags":["Redeemables"]}},"/teams":{"post":{"description":"Creates a new team and assigns the user as the owner. Returns the team.","operationId":"create-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Team","tags":["Team"]}},"/teams/me":{"get":{"description":"Returns the team information and the full list of team members for the currently authenticated user","operationId":"get-my-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get My Team","tags":["Team"]}},"/teams/me/pending-joins":{"get":{"description":"Returns the current user's pending requests for teams.","operationId":"get-my-pending-join-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TeamJoinRequest"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User's Pending Join Requests","tags":["Team"]}},"/teams/{requestId}/accept":{"post":{"description":"Accepts a pending team join request. Only the team owner can perform this action.","operationId":"accept-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Team Join Request","tags":["Team"]}},"/teams/{requestId}/reject":{"post":{"description":"Rejects a pending team join request. Only the team owner can perform this action.","operationId":"reject-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reject Team Join Request","tags":["Team"]}},"/teams/{teamId}":{"get":{"description":"Returns the team information and the full list of team members by team id","operationId":"get-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Team","tags":["Team"]}},"/teams/{teamId}/join":{"post":{"description":"Requests to join a team or fails if user is already on a team.","operationId":"create-join-team-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJoinRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinRequest"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Request to Join Team","tags":["Team"]}},"/teams/{teamId}/kick/{memberId}":{"post":{"description":"Kicks a member from a team. Only the team owner can perform this action.","operationId":"kick-member-from-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"memberId","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Kick Team Member","tags":["Team"]}},"/teams/{teamId}/leave":{"post":{"description":"Leaves a team if the user is on the team.","operationId":"leave-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Leave Team","tags":["Team"]}},"/teams/{teamId}/pending-joins":{"get":{"description":"Returns a team's pending join requests. This is only allowed for the team's owner.","operationId":"get-pending-join-team-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ListJoinRequestsByTeamAndStatusWithUserRow"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Forbidden"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Pending Join Requests for Team","tags":["Team"]}},"/users":{"get":{"description":"Get or search for users by name or email. If no search term is provided, returns all users with pagination.","operationId":"get-users","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"explode":false,"in":"query","name":"search","schema":{"type":"string"}},{"explode":false,"in":"query","name":"limit","schema":{"default":50,"format":"int64","type":"integer"}},{"explode":false,"in":"query","name":"offset","schema":{"default":0,"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Users","tags":["Users"]}},"/users/email/{email}":{"get":{"description":"Returns the user associated with the email","operationId":"get-user-by-email","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Email","tags":["Users"]}},"/users/me":{"get":{"description":"Returns the authenticated user's profile","operationId":"get-me","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserContext"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Me","tags":["Users"]},"patch":{"description":"Updates information of the authenticated user","operationId":"update-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update User","tags":["Users"]}},"/users/me/email-consent":{"patch":{"description":"Updates the user's email consent setting","operationId":"update-email-consent","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailConsentRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Email Consent","tags":["Users"]}},"/users/me/onboarding":{"patch":{"description":"Allows the user to submit information such as name and preferred email, and complete the onboarding process","operationId":"onboard-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Onboard User","tags":["Users"]}},"/users/rfid/{rfid}":{"get":{"description":"Returns the user associated with the RFID","operationId":"get-user-by-rfid","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rfid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By RFID","tags":["Users"]}},"/users/roles/assign":{"post":{"description":"Assigns/modify a user's role","operationId":"assign-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Role","tags":["Users"]}},"/users/roles/batch-assign":{"post":{"description":"Batch assign/modify multiple users' roles","operationId":"batch-assign-roles","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleBatchRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Batch Assign Roles","tags":["Users"]}},"/users/roles/revoke/{userId}":{"post":{"description":"Remove a user's role","operationId":"revoke-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Revoke Role","tags":["Users"]}},"/users/userid/{userId}":{"get":{"description":"Returns the user associated with the user id","operationId":"get-user-by-id","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Id","tags":["Users"]}}}} \ No newline at end of file +{"components":{"schemas":{"ApplicationStatistics":{"additionalProperties":false,"properties":{"ageStats":{"$ref":"#/components/schemas/GetApplicationAgeSplitRow"},"genderStats":{"$ref":"#/components/schemas/GetApplicationGenderSplitRow"},"majorStats":{"items":{"$ref":"#/components/schemas/GetApplicationMajorSplitRow"},"type":["array","null"]},"raceStats":{"items":{"$ref":"#/components/schemas/GetApplicationRaceSplitRow"},"type":["array","null"]},"schoolStats":{"items":{"$ref":"#/components/schemas/GetApplicationSchoolSplitRow"},"type":["array","null"]},"statusStats":{"$ref":"#/components/schemas/GetApplicationStatusSplitRow"}},"required":["genderStats","ageStats","raceStats","majorStats","schoolStats","statusStats"],"type":"object"},"AssignRoleBatchRequest":{"additionalProperties":false,"properties":{"assignments":{"items":{"$ref":"#/components/schemas/AssignRoleRequest"},"type":["array","null"]}},"required":["assignments"],"type":"object"},"AssignRoleRequest":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"role":{"type":"string"},"userID":{"type":["string","null"]}},"required":["email","userID","role"],"type":"object"},"AssignedApplication":{"additionalProperties":false,"properties":{"applicantId":{"type":"string"},"status":{"type":"string"}},"required":["applicantId","status"],"type":"object"},"CheckInRequest":{"additionalProperties":false,"properties":{"rfid":{"type":["string","null"]},"userID":{"type":"string"}},"required":["userID","rfid"],"type":"object"},"CreateJoinRequest":{"additionalProperties":false,"properties":{"message":{"type":["string","null"]}},"required":["message"],"type":"object"},"CreateRedeemableRequest":{"additionalProperties":false,"properties":{"amount":{"format":"int64","minimum":1,"type":"integer"},"maxUserAmount":{"format":"int64","type":"integer"},"name":{"minLength":1,"type":"string"}},"required":["name","amount","maxUserAmount"],"type":"object"},"CreateTeamRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"FormFile":{"additionalProperties":false,"properties":{"ContentType":{"type":"string"},"Filename":{"type":"string"},"IsSet":{"type":"boolean"},"Size":{"format":"int64","type":"integer"}},"required":["ContentType","IsSet","Size","Filename"],"type":"object"},"GetApplicationAgeSplitRow":{"additionalProperties":false,"properties":{"age_18":{"format":"int64","type":"integer"},"age_19":{"format":"int64","type":"integer"},"age_20":{"format":"int64","type":"integer"},"age_21":{"format":"int64","type":"integer"},"age_22":{"format":"int64","type":"integer"},"age_23_plus":{"format":"int64","type":"integer"},"underage":{"format":"int64","type":"integer"}},"required":["underage","age_18","age_19","age_20","age_21","age_22","age_23_plus"],"type":"object"},"GetApplicationGenderSplitRow":{"additionalProperties":false,"properties":{"female":{"format":"int64","type":"integer"},"male":{"format":"int64","type":"integer"},"non_binary":{"format":"int64","type":"integer"},"other":{"format":"int64","type":"integer"}},"required":["male","female","non_binary","other"],"type":"object"},"GetApplicationMajorSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"major":{"type":"string"}},"required":["major","count"],"type":"object"},"GetApplicationRaceSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"race_group":{"type":"string"}},"required":["race_group","count"],"type":"object"},"GetApplicationSchoolSplitRow":{"additionalProperties":false,"properties":{"count":{"format":"int64","type":"integer"},"school":{"type":"string"}},"required":["school","count"],"type":"object"},"GetApplicationStatusSplitRow":{"additionalProperties":false,"properties":{"accepted":{"format":"int64","type":"integer"},"rejected":{"format":"int64","type":"integer"},"started":{"format":"int64","type":"integer"},"submitted":{"format":"int64","type":"integer"},"under_review":{"format":"int64","type":"integer"},"waitlisted":{"format":"int64","type":"integer"},"withdrawn":{"format":"int64","type":"integer"}},"required":["started","submitted","under_review","accepted","rejected","waitlisted","withdrawn"],"type":"object"},"GetAttendeesWithDiscordRow":{"additionalProperties":false,"properties":{"discord_id":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":"string"},"user_id":{"type":"string"}},"required":["discord_id","user_id","name","email"],"type":"object"},"GetRedeemablesRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"total_redeemed":{},"total_stock":{"format":"int32","type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","total_stock","max_user_amount","created_at","updated_at","total_redeemed"],"type":"object"},"Hackathon":{"additionalProperties":false,"properties":{"application_close":{"format":"date-time","type":"string"},"application_open":{"format":"date-time","type":"string"},"application_review_started":{"type":"boolean"},"banner":{"type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"decision_release":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"end_time":{"format":"date-time","type":"string"},"id":{"type":"string"},"is_active":{"type":"boolean"},"location":{"type":["string","null"]},"location_url":{"type":["string","null"]},"max_attendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvp_deadline":{"format":"date-time","type":["string","null"]},"start_time":{"format":"date-time","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","description","location","location_url","max_attendees","application_open","application_close","rsvp_deadline","decision_release","start_time","end_time","is_active","created_at","updated_at","banner","application_review_started"],"type":"object"},"HackerApplication":{"additionalProperties":false,"properties":{"application":{"contentEncoding":"base64","type":"string"},"createdAt":{"format":"date-time","type":"string"},"hackathonId":{"type":"string"},"savedAt":{"format":"date-time","type":"string"},"status":{"type":"string"},"submittedAt":{"format":"date-time","type":["string","null"]},"updatedAt":{"format":"date-time","type":"string"},"userId":{"type":"string"}},"required":["userId","status","application","createdAt","savedAt","updatedAt","submittedAt","hackathonId"],"type":"object"},"ListJoinRequestsByTeamAndStatusWithUserRow":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_email":{"type":["string","null"]},"user_id":{"type":"string"},"user_image":{"type":["string","null"]},"user_name":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at","user_email","user_name","user_image"],"type":"object"},"MemberWithUserInfo":{"additionalProperties":false,"properties":{"email":{"type":["string","null"]},"image":{"type":["string","null"]},"joinedAt":{"format":"date-time","type":"string"},"name":{"type":"string"},"userID":{"type":"string"}},"required":["userID","email","image","name","joinedAt"],"type":"object"},"OnboardingRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferredEmail":{"type":"string"}},"required":["name","preferredEmail"],"type":"object"},"PublicHackathon":{"additionalProperties":false,"properties":{"applicationClose":{"format":"date-time","type":"string"},"applicationOpen":{"format":"date-time","type":"string"},"banner":{"type":["string","null"]},"description":{"type":["string","null"]},"endTime":{"format":"date-time","type":"string"},"id":{"type":"string"},"location":{"type":["string","null"]},"locationUrl":{"type":["string","null"]},"name":{"type":"string"},"rsvpDeadline":{"format":"date-time","type":["string","null"]},"startTime":{"format":"date-time","type":"string"}},"required":["id","name","description","location","locationUrl","applicationOpen","applicationClose","rsvpDeadline","startTime","endTime","banner"],"type":"object"},"Redeemable":{"additionalProperties":false,"properties":{"amount":{"format":"int32","type":"integer"},"created_at":{"format":"date-time","type":"string"},"hackathon_id":{"type":"string"},"id":{"type":"string"},"max_user_amount":{"format":"int32","type":"integer"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","amount","max_user_amount","created_at","updated_at","hackathon_id"],"type":"object"},"ReviewRatings":{"additionalProperties":false,"properties":{"experienceRating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"},"passionRating":{"format":"int64","maxLength":5,"minLength":1,"type":"integer"}},"required":["passionRating","experienceRating"],"type":"object"},"ReviewerAssignment":{"additionalProperties":false,"properties":{"amount":{"format":"int64","type":["integer","null"]},"userID":{"type":"string"}},"required":["userID","amount"],"type":"object"},"SubmitInterestEmailRequest":{"additionalProperties":false,"properties":{"email":{"type":"string"},"source":{"type":["string","null"]}},"required":["email","source"],"type":"object"},"Team":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"hackathon_id":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"owner_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","owner_id","created_at","updated_at","hackathon_id"],"type":"object"},"TeamJoinRequest":{"additionalProperties":false,"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"processed_at":{"format":"date-time","type":["string","null"]},"processed_by_user_id":{"type":"string"},"request_message":{"type":["string","null"]},"status":{"type":"string"},"team_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_id":{"type":"string"}},"required":["id","team_id","user_id","request_message","status","processed_by_user_id","processed_at","created_at","updated_at"],"type":"object"},"TeamWithMembers":{"additionalProperties":false,"properties":{"id":{"type":"string"},"members":{"items":{"$ref":"#/components/schemas/MemberWithUserInfo"},"type":["array","null"]},"name":{"type":"string"},"ownerId":{"type":"string"}},"required":["id","ownerId","name","members"],"type":"object"},"UpdateEmailConsentRequest":{"additionalProperties":false,"properties":{"emailConsent":{"type":"boolean"}},"required":["emailConsent"],"type":"object"},"UpdateHackathonRequest":{"additionalProperties":false,"properties":{"applicationClose":{"format":"date-time","type":"string"},"applicationOpen":{"format":"date-time","type":"string"},"decisionRelease":{"format":"date-time","type":["string","null"]},"description":{"type":["string","null"]},"endTime":{"format":"date-time","type":"string"},"location":{"type":["string","null"]},"locationUrl":{"type":["string","null"]},"maxAttendees":{"format":"int32","type":["integer","null"]},"name":{"type":"string"},"rsvpDeadline":{"format":"date-time","type":["string","null"]},"startTime":{"format":"date-time","type":"string"}},"type":"object"},"UpdateRedeemableRequest":{"additionalProperties":false,"properties":{"maxUserAmount":{"format":"int64","type":"integer"},"name":{"type":"string"},"totalStock":{"format":"int64","type":"integer"}},"type":"object"},"UpdateRedemptionRequest":{"additionalProperties":false,"properties":{"newAmount":{"format":"int64","type":"integer"}},"type":"object"},"UpdateUserRequest":{"additionalProperties":false,"properties":{"name":{"type":"string"},"preferredEmail":{"type":"string"}},"required":["name","preferredEmail"],"type":"object"},"User":{"additionalProperties":false,"properties":{"checked_in_at":{"format":"date-time","type":["string","null"]},"created_at":{"format":"date-time","type":"string"},"email":{"type":["string","null"]},"email_consent":{"type":"boolean"},"email_verified":{"type":"boolean"},"id":{"type":"string"},"image":{"type":["string","null"]},"name":{"type":"string"},"onboarded":{"type":"boolean"},"preferred_email":{"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"type":"string"},"role_assigned_at":{"format":"date-time","type":["string","null"]},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","email","email_verified","onboarded","image","created_at","updated_at","preferred_email","email_consent","checked_in_at","rfid","role_assigned_at","role"],"type":"object"},"UserContext":{"additionalProperties":false,"properties":{"checkedInAt":{"format":"date-time","type":["string","null"]},"email":{"examples":["user@example.com"],"type":["string","null"]},"emailConsent":{"examples":[false],"type":"boolean"},"image":{"examples":["https://cdn.example.com/avatar.png"],"type":["string","null"]},"name":{"examples":["Jane Doe"],"type":"string"},"onboarded":{"examples":[true],"type":"boolean"},"preferredEmail":{"examples":["user.alt@example.com"],"type":["string","null"]},"rfid":{"type":["string","null"]},"role":{"enum":["admin","staff","attendee","applicant","visitor"],"type":"string"},"userId":{"examples":["550e8400-e29b-41d4-a716-446655440000"],"format":"uuid","type":"string"}},"required":["userId","email","preferredEmail","name","onboarded","image","role","emailConsent","rfid","checkedInAt"],"type":"object"}}},"info":{"title":"SwampHacks API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/application":{"get":{"description":"Get the application of the current user","operationId":"get-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HackerApplication"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application","tags":["Application"]}},"/application/accept-acceptance":{"patch":{"description":"Accept an acceptance after being accepted. Sets event role to attendee, from applicant.","operationId":"accept-application-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Application Acceptance","tags":["Application"]}},"/application/assigned":{"get":{"description":"Returns assigned applications and their review progress for the authenticated reviewer","operationId":"get-assigned-applications","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AssignedApplication"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Assigned Applications","tags":["Application"]}},"/application/calculate-admissions":{"post":{"description":"Queues an admission calculation task to the BAT worker","operationId":"calculate-admissions-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Admissions Calculation Request","tags":["Application"]}},"/application/join-waitlist":{"patch":{"description":"Adds a waitlist join time to application. Sets status to waitlisted","operationId":"join-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Join Waitlist","tags":["Application"]}},"/application/release-decisions/{runId}":{"post":{"description":"Releases decisions that were calculated by the worker from a specific run id","operationId":"release-decisions","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"runId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Release Decisions","tags":["Application"]}},"/application/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object.","operationId":"get-download-resume-url","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume Download URL","tags":["Application"]}},"/application/review/assign":{"post":{"description":"Assigns applications to reviewers for the application review process.","operationId":"assign-application-reviewers","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ReviewerAssignment"},"type":["array","null"]}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Application Reviewers","tags":["Application"]}},"/application/review/reset":{"post":{"description":"Resets all application reviews, clearing any existing reviewer assignments.","operationId":"reset-application-reviews","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reset Application Reviews","tags":["Application"]}},"/application/review/{applicantId}":{"post":{"description":"Handles ratings submissions from staff during the application review process","operationId":"submit-application-review","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRatings"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application Review","tags":["Application"]}},"/application/review/{applicantId}/resume":{"get":{"description":"Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review.","operationId":"get-resume","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"applicantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Resume URL (for review process)","tags":["Application"]}},"/application/save":{"post":{"description":"Save user's progress on the application. File/Upload fields are not saved (eg. resumes).","operationId":"save-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{}}}},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Save Application","tags":["Application"]}},"/application/stats":{"get":{"description":"Aggregates applications by race, gender, age, majors, and schools","operationId":"get-application-statistics","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationStatistics"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Application Statistics","tags":["Application"]}},"/application/submit":{"post":{"description":"Submit the application","operationId":"submit-application","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Application","tags":["Application"]}},"/application/transition-waitlisted-applications":{"patch":{"description":"Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected.","operationId":"transition-waitlist","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Transition Waitlisted Applications","tags":["Application"]}},"/application/withdraw-acceptance":{"patch":{"description":"Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected.","operationId":"withdraw-acceptance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Acceptance","tags":["Application"]}},"/application/withdraw-attendance":{"patch":{"description":"Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant.","operationId":"withdraw-attendance","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Withdraw Attendance","tags":["Application"]}},"/auth/callback":{"get":{"description":"Handles the OAuth provider callback, validates state and nonce, and sets the session cookie.","operationId":"oauth-callback","parameters":[{"description":"OAuth authorization code","explode":false,"in":"query","name":"code","required":true,"schema":{"description":"OAuth authorization code","type":"string"}},{"description":"Base64 encoded OAuth state","explode":false,"in":"query","name":"state","required":true,"schema":{"description":"Base64 encoded OAuth state","type":"string"}},{"description":"Auth nonce cookie for CSRF protection","in":"cookie","name":"sh_auth_nonce","required":true,"schema":{"description":"Auth nonce cookie for CSRF protection","type":"string"}},{"description":"Client user agent","in":"header","name":"User-Agent","schema":{"description":"Client user agent","type":"string"}}],"responses":{"204":{"description":"No Content","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"schema":{"type":"string"}}}},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"501":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Implemented"}},"summary":"OAuth Callback","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Logs out the authenticated user by invalidating their session","operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Logout","tags":["Auth"]}},"/hackathon":{"get":{"description":"Returns public information of the hackathon","operationId":"get-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicHackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon","tags":["Hackathon"]},"patch":{"description":"Updates the information of the hackathon","operationId":"update-hackathon","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHackathonRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Hackathon","tags":["Hackathon"]}},"/hackathon/attendees/count":{"get":{"description":"Returns the number of users who is attending the hackathon","operationId":"get-hackathon-attendees-count","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"format":"int64","type":"integer"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees Count","tags":["Hackathon"]}},"/hackathon/attendees/discord":{"get":{"description":"Returns all users with a discord account that is also attending the hackathon","operationId":"get-hackathon-attendees-with-discord","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetAttendeesWithDiscordRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees with Discord","tags":["Hackathon"]}},"/hackathon/attendees/userids":{"get":{"description":"Returns all users ids of users who are attending the hackathon","operationId":"get-hackathon-attendees-userids","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"type":"string"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Attendees User Ids","tags":["Hackathon"]}},"/hackathon/banner":{"delete":{"description":"Deletes the banner","operationId":"delete-banner","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Banner","tags":["Hackathon"]},"post":{"description":"Uploads an image to be used as the banner for the hackathon","operationId":"upload-banner","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"encoding":{"image":{"contentType":"image/png, image/jpeg, image/jpg"}},"schema":{"properties":{"image":{"contentEncoding":"binary","contentMediaType":"application/octet-stream","format":"binary","type":"string"}},"required":["image"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":["string","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Upload Banner","tags":["Hackathon"]}},"/hackathon/checkin":{"post":{"description":"Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet.","operationId":"check-in","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckInRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Check In User","tags":["Hackathon"]}},"/hackathon/detailed":{"get":{"description":"Returns all information of the hackathon","operationId":"get-hackathon-for-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hackathon"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Detailed Hackathon","tags":["Hackathon"]}},"/hackathon/interest":{"post":{"description":"Submits an email to interest/mailing list for the hackathon","operationId":"submit-interest-email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitInterestEmailRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Submit Interest Email","tags":["Hackathon"]}},"/hackathon/staff":{"get":{"description":"Returns the users who are part of the current staff of the hackathon","operationId":"get-hackathon-staff","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Hackathon Staff","tags":["Hackathon"]}},"/ping":{"get":{"description":"Health Check","operationId":"ping","responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Ping","tags":["Misc"]}},"/redeemables":{"get":{"description":"Returns a list of all redeemable items","operationId":"get-redeemables","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/GetRedeemablesRow"},"type":["array","null"]}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Redeemables","tags":["Redeemables"]},"post":{"description":"Creates a new redeemable item","operationId":"create-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRedeemableRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Redeemable"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}":{"delete":{"description":"Deletes a redeemable by id","operationId":"delete-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete Redeemable","tags":["Redeemables"]},"patch":{"description":"Update specific fields (name, stock, max per user) of a redeemable","operationId":"update-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedeemableRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redeemable","tags":["Redeemables"]}},"/redeemables/{redeemableId}/users/{userID}":{"patch":{"description":"Updates a redemption created by the user.","operationId":"update-redemption","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRedemptionRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Redemption","tags":["Redeemables"]},"post":{"description":"Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item","operationId":"redeem-redeemable","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"redeemableId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Redeem Redeemable","tags":["Redeemables"]}},"/teams":{"post":{"description":"Creates a new team and assigns the user as the owner. Returns the team.","operationId":"create-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTeamRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create Team","tags":["Team"]}},"/teams/me":{"get":{"description":"Returns the team information and the full list of team members for the currently authenticated user","operationId":"get-my-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get My Team","tags":["Team"]}},"/teams/me/pending-joins":{"get":{"description":"Returns the current user's pending requests for teams.","operationId":"get-my-pending-join-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TeamJoinRequest"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User's Pending Join Requests","tags":["Team"]}},"/teams/{requestId}/accept":{"post":{"description":"Accepts a pending team join request. Only the team owner can perform this action.","operationId":"accept-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Accept Team Join Request","tags":["Team"]}},"/teams/{requestId}/reject":{"post":{"description":"Rejects a pending team join request. Only the team owner can perform this action.","operationId":"reject-team-join-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Reject Team Join Request","tags":["Team"]}},"/teams/{teamId}":{"get":{"description":"Returns the team information and the full list of team members by team id","operationId":"get-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamWithMembers"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Team","tags":["Team"]}},"/teams/{teamId}/join":{"post":{"description":"Requests to join a team or fails if user is already on a team.","operationId":"create-join-team-request","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJoinRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinRequest"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Request to Join Team","tags":["Team"]}},"/teams/{teamId}/kick/{memberId}":{"post":{"description":"Kicks a member from a team. Only the team owner can perform this action.","operationId":"kick-member-from-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"memberId","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Kick Team Member","tags":["Team"]}},"/teams/{teamId}/leave":{"post":{"description":"Leaves a team if the user is on the team.","operationId":"leave-team","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Leave Team","tags":["Team"]}},"/teams/{teamId}/pending-joins":{"get":{"description":"Returns a team's pending join requests. This is only allowed for the team's owner.","operationId":"get-pending-join-team-requests","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ListJoinRequestsByTeamAndStatusWithUserRow"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Forbidden"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Pending Join Requests for Team","tags":["Team"]}},"/users":{"get":{"description":"Get or search for users by name or email. If no search term is provided, returns all users with pagination.","operationId":"get-users","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"explode":false,"in":"query","name":"search","schema":{"type":"string"}},{"explode":false,"in":"query","name":"limit","schema":{"default":50,"format":"int64","type":"integer"}},{"explode":false,"in":"query","name":"offset","schema":{"default":0,"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/User"},"type":["array","null"]}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Users","tags":["Users"]}},"/users/email/{email}":{"get":{"description":"Returns the user associated with the email","operationId":"get-user-by-email","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Email","tags":["Users"]}},"/users/me":{"get":{"description":"Returns the authenticated user's profile","operationId":"get-me","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserContext"}}},"description":"OK"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get Me","tags":["Users"]},"patch":{"description":"Updates information of the authenticated user","operationId":"update-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update User","tags":["Users"]}},"/users/me/email-consent":{"patch":{"description":"Updates the user's email consent setting","operationId":"update-email-consent","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailConsentRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update Email Consent","tags":["Users"]}},"/users/me/onboarding":{"patch":{"description":"Allows the user to submit information such as name and preferred email, and complete the onboarding process","operationId":"onboard-user","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingRequest"}}},"required":true},"responses":{"200":{"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Onboard User","tags":["Users"]}},"/users/rfid/{rfid}":{"get":{"description":"Returns the user associated with the RFID","operationId":"get-user-by-rfid","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rfid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By RFID","tags":["Users"]}},"/users/roles/assign":{"post":{"description":"Assigns/modify a user's role","operationId":"assign-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Assign Role","tags":["Users"]}},"/users/roles/batch-assign":{"post":{"description":"Batch assign/modify multiple users' roles","operationId":"batch-assign-roles","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleBatchRequest"}}},"required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Batch Assign Roles","tags":["Users"]}},"/users/roles/revoke/{userID}":{"post":{"description":"Remove a user's role","operationId":"revoke-role","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userID","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Revoke Role","tags":["Users"]}},"/users/userid/{userID}":{"get":{"description":"Returns the user associated with the user id","operationId":"get-user-by-id","parameters":[{"description":"Session cookie used to authenticate the user","in":"cookie","name":"sh_session_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"userID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"401":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get User By Id","tags":["Users"]}}}} \ No newline at end of file From 9d1f32e55307d23ddc08881fd96c5e074e9d5096 Mon Sep 17 00:00:00 2001 From: hieu Date: Tue, 31 Mar 2026 22:50:35 -0400 Subject: [PATCH 08/11] chore: update base image to golang:1.26-alpine in Dockerfiles --- apps/api/cmd/BAT_worker/Dockerfile | 2 +- apps/api/cmd/email_worker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/cmd/BAT_worker/Dockerfile b/apps/api/cmd/BAT_worker/Dockerfile index f1a8eaa3..a9ff813c 100644 --- a/apps/api/cmd/BAT_worker/Dockerfile +++ b/apps/api/cmd/BAT_worker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS base +FROM golang:1.26-alpine AS base WORKDIR /app diff --git a/apps/api/cmd/email_worker/Dockerfile b/apps/api/cmd/email_worker/Dockerfile index 268eac2b..7f98d7fb 100644 --- a/apps/api/cmd/email_worker/Dockerfile +++ b/apps/api/cmd/email_worker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS base +FROM golang:1.26-alpine AS base WORKDIR /app From a9ec2a19d093705871100c6eaa9385ba33f1c776 Mon Sep 17 00:00:00 2001 From: hieu Date: Tue, 31 Mar 2026 22:57:02 -0400 Subject: [PATCH 09/11] non-breaking refactor for frontend structure --- apps/web/README.md | 12 +- apps/web/package.json | 1 - apps/web/pnpm-lock.yaml | 72 -- .../src/components/ui/Badge/Badge.test.tsx | 38 - .../src/components/ui/Button/Button.test.tsx | 12 - apps/web/src/components/ui/Card/Card.test.tsx | 15 - .../ui/Seperator/Seperator.test.tsx | 11 - .../FormBuilder/scripts/countries.json | 245 ------ .../stories/applicationFormExample.json | 93 --- apps/web/src/features/README.md | 1 - .../tanstack-query/root-provider.tsx | 27 - apps/web/src/lib/authClient.ts | 2 +- .../{query.ts => tanstack-query-client.ts} | 0 apps/web/src/main.tsx | 12 +- .../Application}/ApplicationAgeChart.tsx | 0 .../Application}/ApplicationForm.tsx | 6 +- .../Application}/ApplicationGenderChart.tsx | 0 .../Application}/ApplicationMajorsChart.tsx | 0 .../Application}/ApplicationOverview.tsx | 4 +- .../Application}/ApplicationRaceChart.tsx | 0 .../Application}/ApplicationSchoolsChart.tsx | 0 .../Application}/ApplicationStatistics.tsx | 6 +- .../Application}/ApplicationStats.tsx | 4 +- .../Application}/ApplicationStatus.tsx | 6 +- .../Application}/SubmissionsChart.tsx | 10 +- .../Application}/bell.svg | 0 .../Application}/cloud.svg | 0 .../Application}/cloud2.svg | 0 .../Application}/cloud3.svg | 0 .../Application}/cloud4.svg | 0 .../Application/hooks/useApplication.ts | 0 .../hooks/useApplicationStatistics.ts | 0 .../hooks/useAssignedApplication.ts | 0 .../Application/hooks/useMyApplication.ts | 0 .../Application}/tower.svg | 0 .../ApplicationReview}/ResetReviewModal.tsx | 0 .../Review/ApplicationReviewContainer.tsx | 4 +- .../Review/ApplicationReviewPage.tsx | 0 .../Review/EssayResponse.tsx | 0 .../Review/RatingFields.tsx | 0 .../Review/ReviewNavigation.tsx | 0 .../ReviewNotStarted/ReviewNotStarted.tsx | 6 +- .../ReviewerAssignmentModal.tsx | 6 +- .../ReviewNotStarted/ReviewerList.tsx | 2 +- .../ReviewNotStarted/StartReviewButton.tsx | 6 +- .../ReviewNotStarted/SummaryFooter.tsx | 0 .../ApplicationReview/hooks/useAppResume.ts | 0 .../hooks/useAppReviewActions.ts | 4 +- .../hooks/useAppReviewAdminActions.ts | 2 +- .../hooks/useAppReviewProgress.ts | 0 .../hooks/useAppReviewTutorial.ts | 0 .../hooks/useAssignedApplications.ts | 0 .../ApplicationReview/hooks/useRatings.ts | 0 .../components => modules/Auth}/Login.tsx | 0 .../Auth/hooks/useAuth.ts | 2 +- .../src/{features => modules}/Auth/types.ts | 0 .../CheckIn/components/CheckInBadge.tsx | 0 .../CheckIn/components/CheckInModal.tsx | 2 +- .../CheckIn/hooks/useUserEventInfo.ts | 0 .../Dashboard}/ApplicantAppShell.tsx | 5 +- .../Dashboard}/AttendeeAppShell.tsx | 0 .../Dashboard}/StaffAppShell.tsx | 0 .../Event/api/getEvent.ts | 0 .../Event/api/getUserEventRole.ts | 0 .../Event/api/updateEvent.ts | 0 .../Event/applicationStatus.ts | 0 .../EventAcceptanceWithdrawalModal.tsx | 2 +- .../EventAttendanceWithdrawalModal.tsx | 2 +- .../Event/components/EventBadge.tsx | 0 .../Event/components/EventBannerUploader.tsx | 0 .../Event/components/EventButton.tsx | 0 .../Event/components/EventCard.tsx | 0 .../Event/components/EventDetailsModal.tsx | 0 .../Event/components/EventSettingsForm.tsx | 0 .../Event/components/EventWaitlistModal.tsx | 2 +- .../Event/components/placeholder.jpg | Bin .../components/stories/EventBadge.stories.tsx | 0 .../stories/EventButton.stories.tsx | 0 .../components/stories/EventCard.stories.tsx | 0 .../Event/hooks/useEvent.ts | 0 .../Event/hooks/useEventBannerActions.ts | 0 .../Event/hooks/useEventsWithUserInfo.ts | 0 .../Event/hooks/useUpdateEvent.ts | 0 .../Event/hooks/useUpdateEventForm.tsx | 0 .../Event/schemas/event.ts | 0 .../Event/utils/mapper.ts | 0 .../EventAdmin}/AddStaffModal.tsx | 6 +- .../EventAdmin}/DeleteStaffDialog.tsx | 4 +- .../EventAdmin}/RoleBadge.tsx | 0 .../EventAdmin}/StaffTable.tsx | 2 +- .../EventAdmin}/UserSideDrawer.tsx | 2 +- .../EventAdmin}/UserTable.tsx | 4 +- .../EventAdmin/hooks/useStaffActions.ts | 2 +- .../EventAdmin/hooks/useUrlTableState.ts | 0 .../EventOverview}/AttendeeOverview.tsx | 2 +- .../EventOverview}/EventDetails.tsx | 2 +- .../EventOverview}/StaffOverview.tsx | 6 +- .../EventOverview/hooks/useEventOverview.ts | 0 .../FormBuilder/build.tsx | 45 +- .../FormBuilder/errorMessage.ts | 0 .../FormBuilder/formSchema.ts | 6 +- .../FormBuilder/icons.ts | 0 .../FormBuilder/questions/baseQuestion.ts | 2 +- .../FormBuilder/questions/checkbox.ts | 4 +- .../questions/createQuestionItem.ts | 0 .../FormBuilder/questions/date.ts | 4 +- .../FormBuilder/questions/index.ts | 0 .../FormBuilder/questions/multipleChoice.ts | 4 +- .../FormBuilder/questions/multiselect.ts | 4 +- .../FormBuilder/questions/number.ts | 4 +- .../FormBuilder/questions/paragraph.ts | 4 +- .../FormBuilder/questions/select.ts | 4 +- .../FormBuilder/questions/shortAnswer.ts | 6 +- .../FormBuilder/questions/upload.ts | 4 +- .../FormBuilder/questions/url.ts | 6 +- .../FormBuilder/scripts/countries.json | 245 ++++++ .../FormBuilder/scripts/majors.csv | 0 .../FormBuilder/scripts/majors.json | 0 .../FormBuilder/scripts/parseCSV.js | 0 .../FormBuilder/scripts/schools.csv | 0 .../FormBuilder/scripts/schools.json | 0 .../stories/applicationFormExample.json | 696 ++++++++++++++++++ .../FormBuilder/stories/example.json | 0 .../FormBuilder/stories/example.stories.tsx | 0 .../FormBuilder/test/build.test.ts | 2 +- .../FormBuilder/test/invalid.json | 0 .../test/invalidMissingMetadata.json | 0 .../invalidMoreThanTwoQuestionsInLayout.json | 0 .../test/invalidNestedLayouts.json | 0 .../test/invalidNestedSections.json | 0 .../test/invalidUnknownFieldType.json | 0 .../FormBuilder/test/valid.json | 0 .../FormBuilder/types.ts | 0 .../NotFound/NotFoundPage.tsx | 0 .../Onboarding}/OnboardingModal.tsx | 0 .../EventManager}/AddEventModal.tsx | 2 +- .../EventManager}/AddStaffForm.tsx | 2 +- .../EventManager}/DeleteEventDialog.tsx | 2 +- .../EventManager}/EventDetailsCard.tsx | 0 .../EventManager}/ManageEventStaffDialog.tsx | 4 +- .../EventManager}/StaffTable.tsx | 0 .../hooks/useAdminEventActions.ts | 0 .../EventManager/hooks/useAdminEvents.ts | 0 .../hooks/useAdminStaffActions.ts | 0 .../EventManager/hooks/useCreateAdminEvent.ts | 2 +- .../EventManager/hooks/useEventStaffUsers.ts | 0 .../EventManager/hooks/useEventUsers.ts | 0 .../Redeemables}/CreateRedeemableModal.tsx | 2 +- .../Redeemables}/DeleteRedeemableModal.tsx | 0 .../Redeemables}/RedeemableCard.tsx | 0 .../Redeemables}/RedeemableDetailsModal.tsx | 2 +- .../Redeemables/hooks/useRedeemables.ts | 0 .../Settings}/SettingsPage.tsx | 2 +- .../Settings/hooks/useSettingsActions.tsx | 0 .../Team}/MyTeamCard.tsx | 4 +- .../Team}/NoTeamCard.tsx | 9 +- .../components => modules/Team}/TeamCard.tsx | 4 +- .../Team}/TeamInvitationCard.tsx | 0 .../Team}/TeamInvitationSection.tsx | 0 .../Team}/TeamJoinRequestCard.tsx | 0 .../Team}/TeamJoinRequestSection.tsx | 4 +- .../Team/hooks/useEventTeams.ts | 4 +- .../Team/hooks/useJoinRequestActions.ts | 0 .../Team/hooks/useMyPendingJoinRequests.ts | 0 .../Team/hooks/useMyTeam.ts | 0 .../Team/hooks/useTeamActions.ts | 0 .../Team/hooks/useTeamPendingJoinRequests.ts | 0 .../Users/hooks/useUsers.ts | 0 apps/web/src/routes/__root.tsx | 2 +- .../src/routes/_protected/_user/portal.tsx | 6 +- .../_protected/admin/events-management.tsx | 6 +- .../events/$eventId/application.tsx | 4 +- .../_admin/application-decisions.tsx | 2 +- .../dashboard/_admin/event-settings.tsx | 6 +- .../dashboard/_admin/staff-management.tsx | 6 +- .../dashboard/_admin/user-management.tsx | 4 +- .../_applicant/application-status.tsx | 2 +- .../dashboard/_staff/application-review.tsx | 10 +- .../_staff/application-statistics.tsx | 2 +- .../$eventId/dashboard/_staff/check-in.tsx | 2 +- .../$eventId/dashboard/_staff/redeemables.tsx | 6 +- .../events/$eventId/dashboard/index.tsx | 4 +- .../events/$eventId/dashboard/layout.tsx | 12 +- .../events/$eventId/dashboard/my-team.tsx | 8 +- .../$eventId/dashboard/teams-explorer.tsx | 8 +- apps/web/src/routes/_protected/settings.tsx | 2 +- apps/web/src/routes/index.tsx | 2 +- 187 files changed, 1128 insertions(+), 701 deletions(-) delete mode 100644 apps/web/src/components/ui/Badge/Badge.test.tsx delete mode 100644 apps/web/src/components/ui/Button/Button.test.tsx delete mode 100644 apps/web/src/components/ui/Card/Card.test.tsx delete mode 100644 apps/web/src/components/ui/Seperator/Seperator.test.tsx delete mode 100644 apps/web/src/features/FormBuilder/scripts/countries.json delete mode 100644 apps/web/src/features/FormBuilder/stories/applicationFormExample.json delete mode 100644 apps/web/src/features/README.md delete mode 100644 apps/web/src/integrations/tanstack-query/root-provider.tsx rename apps/web/src/lib/{query.ts => tanstack-query-client.ts} (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationAgeChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationForm.tsx (97%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationGenderChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationMajorsChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationOverview.tsx (94%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationRaceChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationSchoolsChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationStatistics.tsx (93%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationStats.tsx (93%) rename apps/web/src/{features/Application/components => modules/Application}/ApplicationStatus.tsx (97%) rename apps/web/src/{features/Application/components => modules/Application}/SubmissionsChart.tsx (100%) rename apps/web/src/{features/Application/components => modules/Application}/bell.svg (100%) rename apps/web/src/{features/Application/components => modules/Application}/cloud.svg (100%) rename apps/web/src/{features/Application/components => modules/Application}/cloud2.svg (100%) rename apps/web/src/{features/Application/components => modules/Application}/cloud3.svg (100%) rename apps/web/src/{features/Application/components => modules/Application}/cloud4.svg (100%) rename apps/web/src/{features => modules}/Application/hooks/useApplication.ts (100%) rename apps/web/src/{features => modules}/Application/hooks/useApplicationStatistics.ts (100%) rename apps/web/src/{features => modules}/Application/hooks/useAssignedApplication.ts (100%) rename apps/web/src/{features => modules}/Application/hooks/useMyApplication.ts (100%) rename apps/web/src/{features/Application/components => modules/Application}/tower.svg (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ResetReviewModal.tsx (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/Review/ApplicationReviewContainer.tsx (97%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/Review/ApplicationReviewPage.tsx (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/Review/EssayResponse.tsx (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/Review/RatingFields.tsx (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/Review/ReviewNavigation.tsx (100%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ReviewNotStarted/ReviewNotStarted.tsx (76%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ReviewNotStarted/ReviewerAssignmentModal.tsx (91%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ReviewNotStarted/ReviewerList.tsx (96%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ReviewNotStarted/StartReviewButton.tsx (83%) rename apps/web/src/{features/ApplicationReview/components => modules/ApplicationReview}/ReviewNotStarted/SummaryFooter.tsx (100%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAppResume.ts (100%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAppReviewActions.ts (92%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAppReviewAdminActions.ts (96%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAppReviewProgress.ts (100%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAppReviewTutorial.ts (100%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useAssignedApplications.ts (100%) rename apps/web/src/{features => modules}/ApplicationReview/hooks/useRatings.ts (100%) rename apps/web/src/{features/Auth/components => modules/Auth}/Login.tsx (100%) rename apps/web/src/{features => modules}/Auth/hooks/useAuth.ts (95%) rename apps/web/src/{features => modules}/Auth/types.ts (100%) rename apps/web/src/{features => modules}/CheckIn/components/CheckInBadge.tsx (100%) rename apps/web/src/{features => modules}/CheckIn/components/CheckInModal.tsx (98%) rename apps/web/src/{features => modules}/CheckIn/hooks/useUserEventInfo.ts (100%) rename apps/web/src/{features/Dashboard/components => modules/Dashboard}/ApplicantAppShell.tsx (94%) rename apps/web/src/{features/Dashboard/components => modules/Dashboard}/AttendeeAppShell.tsx (100%) rename apps/web/src/{features/Dashboard/components => modules/Dashboard}/StaffAppShell.tsx (100%) rename apps/web/src/{features => modules}/Event/api/getEvent.ts (100%) rename apps/web/src/{features => modules}/Event/api/getUserEventRole.ts (100%) rename apps/web/src/{features => modules}/Event/api/updateEvent.ts (100%) rename apps/web/src/{features => modules}/Event/applicationStatus.ts (100%) rename apps/web/src/{features => modules}/Event/components/EventAcceptanceWithdrawalModal.tsx (96%) rename apps/web/src/{features => modules}/Event/components/EventAttendanceWithdrawalModal.tsx (96%) rename apps/web/src/{features => modules}/Event/components/EventBadge.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventBannerUploader.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventButton.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventCard.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventDetailsModal.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventSettingsForm.tsx (100%) rename apps/web/src/{features => modules}/Event/components/EventWaitlistModal.tsx (96%) rename apps/web/src/{features => modules}/Event/components/placeholder.jpg (100%) rename apps/web/src/{features => modules}/Event/components/stories/EventBadge.stories.tsx (100%) rename apps/web/src/{features => modules}/Event/components/stories/EventButton.stories.tsx (100%) rename apps/web/src/{features => modules}/Event/components/stories/EventCard.stories.tsx (100%) rename apps/web/src/{features => modules}/Event/hooks/useEvent.ts (100%) rename apps/web/src/{features => modules}/Event/hooks/useEventBannerActions.ts (100%) rename apps/web/src/{features => modules}/Event/hooks/useEventsWithUserInfo.ts (100%) rename apps/web/src/{features => modules}/Event/hooks/useUpdateEvent.ts (100%) rename apps/web/src/{features => modules}/Event/hooks/useUpdateEventForm.tsx (100%) rename apps/web/src/{features => modules}/Event/schemas/event.ts (100%) rename apps/web/src/{features => modules}/Event/utils/mapper.ts (100%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/AddStaffModal.tsx (95%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/DeleteStaffDialog.tsx (92%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/RoleBadge.tsx (100%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/StaffTable.tsx (97%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/UserSideDrawer.tsx (77%) rename apps/web/src/{features/EventAdmin/components => modules/EventAdmin}/UserTable.tsx (96%) rename apps/web/src/{features => modules}/EventAdmin/hooks/useStaffActions.ts (95%) rename apps/web/src/{features => modules}/EventAdmin/hooks/useUrlTableState.ts (100%) rename apps/web/src/{features/EventOverview/components => modules/EventOverview}/AttendeeOverview.tsx (98%) rename apps/web/src/{features/EventOverview/components => modules/EventOverview}/EventDetails.tsx (98%) rename apps/web/src/{features/EventOverview/components => modules/EventOverview}/StaffOverview.tsx (85%) rename apps/web/src/{features => modules}/EventOverview/hooks/useEventOverview.ts (100%) rename apps/web/src/{features => modules}/FormBuilder/build.tsx (95%) rename apps/web/src/{features => modules}/FormBuilder/errorMessage.ts (100%) rename apps/web/src/{features => modules}/FormBuilder/formSchema.ts (92%) rename apps/web/src/{features => modules}/FormBuilder/icons.ts (100%) rename apps/web/src/{features => modules}/FormBuilder/questions/baseQuestion.ts (91%) rename apps/web/src/{features => modules}/FormBuilder/questions/checkbox.ts (86%) rename apps/web/src/{features => modules}/FormBuilder/questions/createQuestionItem.ts (100%) rename apps/web/src/{features => modules}/FormBuilder/questions/date.ts (84%) rename apps/web/src/{features => modules}/FormBuilder/questions/index.ts (100%) rename apps/web/src/{features => modules}/FormBuilder/questions/multipleChoice.ts (84%) rename apps/web/src/{features => modules}/FormBuilder/questions/multiselect.ts (89%) rename apps/web/src/{features => modules}/FormBuilder/questions/number.ts (88%) rename apps/web/src/{features => modules}/FormBuilder/questions/paragraph.ts (90%) rename apps/web/src/{features => modules}/FormBuilder/questions/select.ts (90%) rename apps/web/src/{features => modules}/FormBuilder/questions/shortAnswer.ts (88%) rename apps/web/src/{features => modules}/FormBuilder/questions/upload.ts (95%) rename apps/web/src/{features => modules}/FormBuilder/questions/url.ts (88%) create mode 100644 apps/web/src/modules/FormBuilder/scripts/countries.json rename apps/web/src/{features => modules}/FormBuilder/scripts/majors.csv (100%) rename apps/web/src/{features => modules}/FormBuilder/scripts/majors.json (100%) rename apps/web/src/{features => modules}/FormBuilder/scripts/parseCSV.js (100%) rename apps/web/src/{features => modules}/FormBuilder/scripts/schools.csv (100%) rename apps/web/src/{features => modules}/FormBuilder/scripts/schools.json (100%) create mode 100644 apps/web/src/modules/FormBuilder/stories/applicationFormExample.json rename apps/web/src/{features => modules}/FormBuilder/stories/example.json (100%) rename apps/web/src/{features => modules}/FormBuilder/stories/example.stories.tsx (100%) rename apps/web/src/{features => modules}/FormBuilder/test/build.test.ts (97%) rename apps/web/src/{features => modules}/FormBuilder/test/invalid.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/invalidMissingMetadata.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/invalidMoreThanTwoQuestionsInLayout.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/invalidNestedLayouts.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/invalidNestedSections.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/invalidUnknownFieldType.json (100%) rename apps/web/src/{features => modules}/FormBuilder/test/valid.json (100%) rename apps/web/src/{features => modules}/FormBuilder/types.ts (100%) rename apps/web/src/{features => modules}/NotFound/NotFoundPage.tsx (100%) rename apps/web/src/{features/Onboarding/components => modules/Onboarding}/OnboardingModal.tsx (100%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/AddEventModal.tsx (98%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/AddStaffForm.tsx (98%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/DeleteEventDialog.tsx (96%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/EventDetailsCard.tsx (100%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/ManageEventStaffDialog.tsx (95%) rename apps/web/src/{features/PlatformAdmin/EventManager/components => modules/PlatformAdmin/EventManager}/StaffTable.tsx (100%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useAdminEventActions.ts (100%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useAdminEvents.ts (100%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useAdminStaffActions.ts (100%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts (95%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useEventStaffUsers.ts (100%) rename apps/web/src/{features => modules}/PlatformAdmin/EventManager/hooks/useEventUsers.ts (100%) rename apps/web/src/{features/Redeemables/components => modules/Redeemables}/CreateRedeemableModal.tsx (98%) rename apps/web/src/{features/Redeemables/components => modules/Redeemables}/DeleteRedeemableModal.tsx (100%) rename apps/web/src/{features/Redeemables/components => modules/Redeemables}/RedeemableCard.tsx (100%) rename apps/web/src/{features/Redeemables/components => modules/Redeemables}/RedeemableDetailsModal.tsx (99%) rename apps/web/src/{features => modules}/Redeemables/hooks/useRedeemables.ts (100%) rename apps/web/src/{features/Settings/components => modules/Settings}/SettingsPage.tsx (99%) rename apps/web/src/{features => modules}/Settings/hooks/useSettingsActions.tsx (100%) rename apps/web/src/{features/Team/components => modules/Team}/MyTeamCard.tsx (97%) rename apps/web/src/{features/Team/components => modules/Team}/NoTeamCard.tsx (94%) rename apps/web/src/{features/Team/components => modules/Team}/TeamCard.tsx (94%) rename apps/web/src/{features/Team/components => modules/Team}/TeamInvitationCard.tsx (100%) rename apps/web/src/{features/Team/components => modules/Team}/TeamInvitationSection.tsx (100%) rename apps/web/src/{features/Team/components => modules/Team}/TeamJoinRequestCard.tsx (100%) rename apps/web/src/{features/Team/components => modules/Team}/TeamJoinRequestSection.tsx (91%) rename apps/web/src/{features => modules}/Team/hooks/useEventTeams.ts (85%) rename apps/web/src/{features => modules}/Team/hooks/useJoinRequestActions.ts (100%) rename apps/web/src/{features => modules}/Team/hooks/useMyPendingJoinRequests.ts (100%) rename apps/web/src/{features => modules}/Team/hooks/useMyTeam.ts (100%) rename apps/web/src/{features => modules}/Team/hooks/useTeamActions.ts (100%) rename apps/web/src/{features => modules}/Team/hooks/useTeamPendingJoinRequests.ts (100%) rename apps/web/src/{features => modules}/Users/hooks/useUsers.ts (100%) diff --git a/apps/web/README.md b/apps/web/README.md index 34ac0125..259370dd 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,18 +1,16 @@ -# SwampHacks Event Management Portal +# SwampHacks Portal Frontend -### Overview - -This is the official event management portal for SwampHacks, built with modern web technologies including **Vite**, **React**, and **TypeScript**. It leverages: +Built with **Vite**, **React**, and **TypeScript**. It uses: - 🧭 [TanStack Router](https://tanstack.com/router/latest) for routing -- 🔄 [TanStack Query](https://tanstack.com/query/latest) + [Axios](https://axios-http.com/) for data fetching and caching +- 🔄 [TanStack Query](https://tanstack.com/query/latest) for data fetching - 🧩 [React Aria](https://react-spectrum.adobe.com/react-aria/index.html) for accessible and customizable UI components ### Setup Instructions To get started: -1. Clone the repo and make sure [pNPm](https://pnpm.io/) is installed on your system. +1. Clone the repo and make sure [pnpm](https://pnpm.io/) is installed on your system. ```bash git clone https://github.com/swamphacks/core.git @@ -31,7 +29,7 @@ pnpm install cp .env.example .env ``` -Fill in the required keys and tokens in your new `.env` file. The prefix `SWAMPHACKS` is required for the environment variables to load properly. +Fill in the required keys and tokens in your new `.env` file. The prefix `VITE_` is required for the environment variables to load properly. 4. Finally, launch the app diff --git a/apps/web/package.json b/apps/web/package.json index b1e8361c..e02fb649 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -37,7 +37,6 @@ "@tanstack/react-table": "^8.21.3", "@uidotdev/usehooks": "^2.4.1", "@yudiel/react-qr-scanner": "^2.5.0", - "axios": "^1.9.0", "browser-image-compression": "^2.0.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 117b9e6f..30430856 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: '@yudiel/react-qr-scanner': specifier: ^2.5.0 version: 2.5.0(@types/emscripten@1.41.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - axios: - specifier: ^1.9.0 - version: 1.10.0 browser-image-compression: specifier: ^2.0.2 version: 2.0.2 @@ -2497,16 +2494,10 @@ packages: ast-v8-to-istanbul@0.3.3: resolution: {integrity: sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.10.0: - resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} - babel-dead-code-elimination@1.0.10: resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} @@ -2666,10 +2657,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -2767,10 +2754,6 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2865,10 +2848,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -3036,15 +3015,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -3053,10 +3023,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} - engines: {node: '>= 6'} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3827,9 +3793,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -7525,20 +7488,10 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 - asynckit@0.4.0: {} - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axios@1.10.0: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.3 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - babel-dead-code-elimination@1.0.10: dependencies: '@babel/core': 7.27.7 @@ -7698,10 +7651,6 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@13.1.0: {} commander@2.20.3: {} @@ -7783,8 +7732,6 @@ snapshots: define-lazy-prop@2.0.0: {} - delayed-stream@1.0.0: {} - dequal@2.0.3: {} detect-libc@2.0.4: {} @@ -7864,13 +7811,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - esbuild-register@3.6.0(esbuild@0.25.5): dependencies: debug: 4.4.1(supports-color@10.0.0) @@ -8080,8 +8020,6 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -8091,14 +8029,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - fsevents@2.3.2: optional: true @@ -8804,8 +8734,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proxy-from-env@1.1.0: {} - punycode@2.3.1: {} qr.js@0.0.0: {} diff --git a/apps/web/src/components/ui/Badge/Badge.test.tsx b/apps/web/src/components/ui/Badge/Badge.test.tsx deleted file mode 100644 index f367c79b..00000000 --- a/apps/web/src/components/ui/Badge/Badge.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import { Badge } from "."; - -describe("Badge component", () => { - it("renders with correct text", () => { - render(A Badge!); - const badge = screen.getByText(/A Badge!/i); - expect(badge).toBeInTheDocument(); - }); - - it("renders with correct icon and text", () => { - render( - - - - - A Badge! - , - ); - let badge = screen.getByTestId("badge-icon"); - expect(badge).toBeInTheDocument(); - - badge = screen.getByText(/A Badge!/i); - expect(badge).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/components/ui/Button/Button.test.tsx b/apps/web/src/components/ui/Button/Button.test.tsx deleted file mode 100644 index 90dc81b8..00000000 --- a/apps/web/src/components/ui/Button/Button.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// src/components/Button.test.tsx -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import { Button } from "."; - -describe("Button component", () => { - it("renders with correct text", () => { - render(); - const button = screen.getByText(/Click Me!/i); - expect(button).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/components/ui/Card/Card.test.tsx b/apps/web/src/components/ui/Card/Card.test.tsx deleted file mode 100644 index 1ab197ba..00000000 --- a/apps/web/src/components/ui/Card/Card.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import { Card } from "."; - -describe("Card component", () => { - it("renders with correct title", () => { - render( - -

Title

-
, - ); - const card = screen.getByText(/Title/i); - expect(card).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/components/ui/Seperator/Seperator.test.tsx b/apps/web/src/components/ui/Seperator/Seperator.test.tsx deleted file mode 100644 index 3e8d01b5..00000000 --- a/apps/web/src/components/ui/Seperator/Seperator.test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import { Separator } from "."; - -describe("Separator component", () => { - it("renders with correct title", () => { - render(); - const separator = screen.getByTestId("separator"); - expect(separator).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/features/FormBuilder/scripts/countries.json b/apps/web/src/features/FormBuilder/scripts/countries.json deleted file mode 100644 index 6ca635bb..00000000 --- a/apps/web/src/features/FormBuilder/scripts/countries.json +++ /dev/null @@ -1,245 +0,0 @@ -[ - {"name": "Afghanistan", "code": "AF"}, - {"name": "Åland Islands", "code": "AX"}, - {"name": "Albania", "code": "AL"}, - {"name": "Algeria", "code": "DZ"}, - {"name": "American Samoa", "code": "AS"}, - {"name": "AndorrA", "code": "AD"}, - {"name": "Angola", "code": "AO"}, - {"name": "Anguilla", "code": "AI"}, - {"name": "Antarctica", "code": "AQ"}, - {"name": "Antigua and Barbuda", "code": "AG"}, - {"name": "Argentina", "code": "AR"}, - {"name": "Armenia", "code": "AM"}, - {"name": "Aruba", "code": "AW"}, - {"name": "Australia", "code": "AU"}, - {"name": "Austria", "code": "AT"}, - {"name": "Azerbaijan", "code": "AZ"}, - {"name": "Bahamas", "code": "BS"}, - {"name": "Bahrain", "code": "BH"}, - {"name": "Bangladesh", "code": "BD"}, - {"name": "Barbados", "code": "BB"}, - {"name": "Belarus", "code": "BY"}, - {"name": "Belgium", "code": "BE"}, - {"name": "Belize", "code": "BZ"}, - {"name": "Benin", "code": "BJ"}, - {"name": "Bermuda", "code": "BM"}, - {"name": "Bhutan", "code": "BT"}, - {"name": "Bolivia", "code": "BO"}, - {"name": "Bosnia and Herzegovina", "code": "BA"}, - {"name": "Botswana", "code": "BW"}, - {"name": "Bouvet Island", "code": "BV"}, - {"name": "Brazil", "code": "BR"}, - {"name": "British Indian Ocean Territory", "code": "IO"}, - {"name": "Brunei Darussalam", "code": "BN"}, - {"name": "Bulgaria", "code": "BG"}, - {"name": "Burkina Faso", "code": "BF"}, - {"name": "Burundi", "code": "BI"}, - {"name": "Cambodia", "code": "KH"}, - {"name": "Cameroon", "code": "CM"}, - {"name": "Canada", "code": "CA"}, - {"name": "Cape Verde", "code": "CV"}, - {"name": "Cayman Islands", "code": "KY"}, - {"name": "Central African Republic", "code": "CF"}, - {"name": "Chad", "code": "TD"}, - {"name": "Chile", "code": "CL"}, - {"name": "China", "code": "CN"}, - {"name": "Christmas Island", "code": "CX"}, - {"name": "Cocos (Keeling) Islands", "code": "CC"}, - {"name": "Colombia", "code": "CO"}, - {"name": "Comoros", "code": "KM"}, - {"name": "Congo", "code": "CG"}, - {"name": "Congo, The Democratic Republic of the", "code": "CD"}, - {"name": "Cook Islands", "code": "CK"}, - {"name": "Costa Rica", "code": "CR"}, - {"name": "Cote D\"Ivoire", "code": "CI"}, - {"name": "Croatia", "code": "HR"}, - {"name": "Cuba", "code": "CU"}, - {"name": "Cyprus", "code": "CY"}, - {"name": "Czech Republic", "code": "CZ"}, - {"name": "Denmark", "code": "DK"}, - {"name": "Djibouti", "code": "DJ"}, - {"name": "Dominica", "code": "DM"}, - {"name": "Dominican Republic", "code": "DO"}, - {"name": "Ecuador", "code": "EC"}, - {"name": "Egypt", "code": "EG"}, - {"name": "El Salvador", "code": "SV"}, - {"name": "Equatorial Guinea", "code": "GQ"}, - {"name": "Eritrea", "code": "ER"}, - {"name": "Estonia", "code": "EE"}, - {"name": "Ethiopia", "code": "ET"}, - {"name": "Falkland Islands (Malvinas)", "code": "FK"}, - {"name": "Faroe Islands", "code": "FO"}, - {"name": "Fiji", "code": "FJ"}, - {"name": "Finland", "code": "FI"}, - {"name": "France", "code": "FR"}, - {"name": "French Guiana", "code": "GF"}, - {"name": "French Polynesia", "code": "PF"}, - {"name": "French Southern Territories", "code": "TF"}, - {"name": "Gabon", "code": "GA"}, - {"name": "Gambia", "code": "GM"}, - {"name": "Georgia", "code": "GE"}, - {"name": "Germany", "code": "DE"}, - {"name": "Ghana", "code": "GH"}, - {"name": "Gibraltar", "code": "GI"}, - {"name": "Greece", "code": "GR"}, - {"name": "Greenland", "code": "GL"}, - {"name": "Grenada", "code": "GD"}, - {"name": "Guadeloupe", "code": "GP"}, - {"name": "Guam", "code": "GU"}, - {"name": "Guatemala", "code": "GT"}, - {"name": "Guernsey", "code": "GG"}, - {"name": "Guinea", "code": "GN"}, - {"name": "Guinea-Bissau", "code": "GW"}, - {"name": "Guyana", "code": "GY"}, - {"name": "Haiti", "code": "HT"}, - {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, - {"name": "Holy See (Vatican City State)", "code": "VA"}, - {"name": "Honduras", "code": "HN"}, - {"name": "Hong Kong", "code": "HK"}, - {"name": "Hungary", "code": "HU"}, - {"name": "Iceland", "code": "IS"}, - {"name": "India", "code": "IN"}, - {"name": "Indonesia", "code": "ID"}, - {"name": "Iran, Islamic Republic Of", "code": "IR"}, - {"name": "Iraq", "code": "IQ"}, - {"name": "Ireland", "code": "IE"}, - {"name": "Isle of Man", "code": "IM"}, - {"name": "Israel", "code": "IL"}, - {"name": "Italy", "code": "IT"}, - {"name": "Jamaica", "code": "JM"}, - {"name": "Japan", "code": "JP"}, - {"name": "Jersey", "code": "JE"}, - {"name": "Jordan", "code": "JO"}, - {"name": "Kazakhstan", "code": "KZ"}, - {"name": "Kenya", "code": "KE"}, - {"name": "Kiribati", "code": "KI"}, - {"name": "Korea, Democratic People\"S Republic of", "code": "KP"}, - {"name": "Korea, Republic of", "code": "KR"}, - {"name": "Kuwait", "code": "KW"}, - {"name": "Kyrgyzstan", "code": "KG"}, - {"name": "Lao People\"S Democratic Republic", "code": "LA"}, - {"name": "Latvia", "code": "LV"}, - {"name": "Lebanon", "code": "LB"}, - {"name": "Lesotho", "code": "LS"}, - {"name": "Liberia", "code": "LR"}, - {"name": "Libyan Arab Jamahiriya", "code": "LY"}, - {"name": "Liechtenstein", "code": "LI"}, - {"name": "Lithuania", "code": "LT"}, - {"name": "Luxembourg", "code": "LU"}, - {"name": "Macao", "code": "MO"}, - {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, - {"name": "Madagascar", "code": "MG"}, - {"name": "Malawi", "code": "MW"}, - {"name": "Malaysia", "code": "MY"}, - {"name": "Maldives", "code": "MV"}, - {"name": "Mali", "code": "ML"}, - {"name": "Malta", "code": "MT"}, - {"name": "Marshall Islands", "code": "MH"}, - {"name": "Martinique", "code": "MQ"}, - {"name": "Mauritania", "code": "MR"}, - {"name": "Mauritius", "code": "MU"}, - {"name": "Mayotte", "code": "YT"}, - {"name": "Mexico", "code": "MX"}, - {"name": "Micronesia, Federated States of", "code": "FM"}, - {"name": "Moldova, Republic of", "code": "MD"}, - {"name": "Monaco", "code": "MC"}, - {"name": "Mongolia", "code": "MN"}, - {"name": "Montserrat", "code": "MS"}, - {"name": "Morocco", "code": "MA"}, - {"name": "Mozambique", "code": "MZ"}, - {"name": "Myanmar", "code": "MM"}, - {"name": "Namibia", "code": "NA"}, - {"name": "Nauru", "code": "NR"}, - {"name": "Nepal", "code": "NP"}, - {"name": "Netherlands", "code": "NL"}, - {"name": "Netherlands Antilles", "code": "AN"}, - {"name": "New Caledonia", "code": "NC"}, - {"name": "New Zealand", "code": "NZ"}, - {"name": "Nicaragua", "code": "NI"}, - {"name": "Niger", "code": "NE"}, - {"name": "Nigeria", "code": "NG"}, - {"name": "Niue", "code": "NU"}, - {"name": "Norfolk Island", "code": "NF"}, - {"name": "Northern Mariana Islands", "code": "MP"}, - {"name": "Norway", "code": "NO"}, - {"name": "Oman", "code": "OM"}, - {"name": "Pakistan", "code": "PK"}, - {"name": "Palau", "code": "PW"}, - {"name": "Palestinian Territory, Occupied", "code": "PS"}, - {"name": "Panama", "code": "PA"}, - {"name": "Papua New Guinea", "code": "PG"}, - {"name": "Paraguay", "code": "PY"}, - {"name": "Peru", "code": "PE"}, - {"name": "Philippines", "code": "PH"}, - {"name": "Pitcairn", "code": "PN"}, - {"name": "Poland", "code": "PL"}, - {"name": "Portugal", "code": "PT"}, - {"name": "Puerto Rico", "code": "PR"}, - {"name": "Qatar", "code": "QA"}, - {"name": "Reunion", "code": "RE"}, - {"name": "Romania", "code": "RO"}, - {"name": "Russian Federation", "code": "RU"}, - {"name": "RWANDA", "code": "RW"}, - {"name": "Saint Helena", "code": "SH"}, - {"name": "Saint Kitts and Nevis", "code": "KN"}, - {"name": "Saint Lucia", "code": "LC"}, - {"name": "Saint Pierre and Miquelon", "code": "PM"}, - {"name": "Saint Vincent and the Grenadines", "code": "VC"}, - {"name": "Samoa", "code": "WS"}, - {"name": "San Marino", "code": "SM"}, - {"name": "Sao Tome and Principe", "code": "ST"}, - {"name": "Saudi Arabia", "code": "SA"}, - {"name": "Senegal", "code": "SN"}, - {"name": "Serbia and Montenegro", "code": "CS"}, - {"name": "Seychelles", "code": "SC"}, - {"name": "Sierra Leone", "code": "SL"}, - {"name": "Singapore", "code": "SG"}, - {"name": "Slovakia", "code": "SK"}, - {"name": "Slovenia", "code": "SI"}, - {"name": "Solomon Islands", "code": "SB"}, - {"name": "Somalia", "code": "SO"}, - {"name": "South Africa", "code": "ZA"}, - {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, - {"name": "Spain", "code": "ES"}, - {"name": "Sri Lanka", "code": "LK"}, - {"name": "Sudan", "code": "SD"}, - {"name": "Suriname", "code": "SR"}, - {"name": "Svalbard and Jan Mayen", "code": "SJ"}, - {"name": "Swaziland", "code": "SZ"}, - {"name": "Sweden", "code": "SE"}, - {"name": "Switzerland", "code": "CH"}, - {"name": "Syrian Arab Republic", "code": "SY"}, - {"name": "Taiwan, Province of China", "code": "TW"}, - {"name": "Tajikistan", "code": "TJ"}, - {"name": "Tanzania, United Republic of", "code": "TZ"}, - {"name": "Thailand", "code": "TH"}, - {"name": "Timor-Leste", "code": "TL"}, - {"name": "Togo", "code": "TG"}, - {"name": "Tokelau", "code": "TK"}, - {"name": "Tonga", "code": "TO"}, - {"name": "Trinidad and Tobago", "code": "TT"}, - {"name": "Tunisia", "code": "TN"}, - {"name": "Turkey", "code": "TR"}, - {"name": "Turkmenistan", "code": "TM"}, - {"name": "Turks and Caicos Islands", "code": "TC"}, - {"name": "Tuvalu", "code": "TV"}, - {"name": "Uganda", "code": "UG"}, - {"name": "Ukraine", "code": "UA"}, - {"name": "United Arab Emirates", "code": "AE"}, - {"name": "United Kingdom", "code": "GB"}, - {"name": "United States", "code": "US"}, - {"name": "United States Minor Outlying Islands", "code": "UM"}, - {"name": "Uruguay", "code": "UY"}, - {"name": "Uzbekistan", "code": "UZ"}, - {"name": "Vanuatu", "code": "VU"}, - {"name": "Venezuela", "code": "VE"}, - {"name": "Viet Nam", "code": "VN"}, - {"name": "Virgin Islands, British", "code": "VG"}, - {"name": "Virgin Islands, U.S.", "code": "VI"}, - {"name": "Wallis and Futuna", "code": "WF"}, - {"name": "Western Sahara", "code": "EH"}, - {"name": "Yemen", "code": "YE"}, - {"name": "Zambia", "code": "ZM"}, - {"name": "Zimbabwe", "code": "ZW"} -] \ No newline at end of file diff --git a/apps/web/src/features/FormBuilder/stories/applicationFormExample.json b/apps/web/src/features/FormBuilder/stories/applicationFormExample.json deleted file mode 100644 index f2e10eba..00000000 --- a/apps/web/src/features/FormBuilder/stories/applicationFormExample.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "metadata": { - "title": "SwampHacks XI Application", - "description": "SwampHacks is the University of Florida’s largest annual hackathon. A 36-hour tech event where students from across the country come together to build projects, learn new skills, and connect with fellow innovators." - }, - - "content": [ - { - "type": "section", - "label": "ℹ️ Personal Information", - - "content": [ - { - "type": "layout", - - "content": [ - { - "name": "firstName", - "questionType": "shortAnswer", - "placeholder": "Enter your first name", - "label": "First Name", - "required": true, - "validation": { - "maxLength": 5 - } - }, - - { - "name": "lastName", - "questionType": "shortAnswer", - "placeholder": "Enter your last name", - "label": "Last Name", - "required": true, - "validation": { - "maxLength": 5 - } - } - ] - }, - { - "name": "majors", - "questionType": "multiselect", - "placeholder": "Select your major(s)", - "label": "Major(s)", - "required": true, - "options": { - "data": "majors" - } - }, - { - "name": "school", - "questionType": "select", - "placeholder": "Select your school", - "label": "School", - "required": true, - "searchable": true, - "options": { - "data": "schools" - } - }, - { - "type": "question", - "name": "resume", - "questionType": "upload", - "label": "Upload your resume", - "required": true, - "validation": { - "validMimeTypes": ["application/pdf"] - } - }, - { - "name": "shareInfo", - "questionType": "checkbox", - "label": "I authorize you to share my application/registration information with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH Privacy Policy (https://github.com/MLH/mlh-policies/blob/main/privacy-policy.md). I further agree to the terms of both the MLH Contest Terms and Conditions (https://github.com/MLH/mlh-policies/blob/main/contest-terms.md) and the MLH Privacy Policy (https://github.com/MLH/mlh-policies/blob/main/privacy-policy.md).", - "required": true, - "options": [{ "label": "I agree", "value": "1" }] - }, - { - "name": "test", - "questionType": "checkbox", - "placeholder": "Enter link", - "required": true, - "options": [ - { - "label": "By clicking this, I certify that I am 18 years old or will turn 18 before January 23rd, 2026.", - "value": "test" - } - ] - } - ] - } - ] -} diff --git a/apps/web/src/features/README.md b/apps/web/src/features/README.md deleted file mode 100644 index d5ae1f9d..00000000 --- a/apps/web/src/features/README.md +++ /dev/null @@ -1 +0,0 @@ -TODO: update documentation diff --git a/apps/web/src/integrations/tanstack-query/root-provider.tsx b/apps/web/src/integrations/tanstack-query/root-provider.tsx deleted file mode 100644 index 2e286a71..00000000 --- a/apps/web/src/integrations/tanstack-query/root-provider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - -export function getContext() { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, - }); - - return { - queryClient, - }; -} - -export function Provider({ - children, - queryClient, -}: { - children: React.ReactNode; - queryClient: QueryClient; -}) { - return ( - {children} - ); -} diff --git a/apps/web/src/lib/authClient.ts b/apps/web/src/lib/authClient.ts index 90ac6eb8..7e7c69a2 100644 --- a/apps/web/src/lib/authClient.ts +++ b/apps/web/src/lib/authClient.ts @@ -1,7 +1,7 @@ import Auth from "./auth"; import { authConfig } from "./auth/config"; import { Discord } from "./auth/providers"; -import { queryClient } from "./query"; +import { queryClient } from "./tanstack-query-client"; import { queryKey as useUserQueryKey } from "./auth/hooks/useUser"; export const auth = Auth({ diff --git a/apps/web/src/lib/query.ts b/apps/web/src/lib/tanstack-query-client.ts similarity index 100% rename from apps/web/src/lib/query.ts rename to apps/web/src/lib/tanstack-query-client.ts diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index cdd3bf33..d5d4ca61 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,20 +1,20 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; -import NotFoundPage from "@/features/NotFound/NotFoundPage"; +import NotFoundPage from "@/modules/NotFound/NotFoundPage"; import "./index.css"; import { routeTree } from "./routeTree.gen"; import { ThemeProvider } from "./components/ThemeProvider"; import { auth } from "./lib/authClient"; import { ToastContainer } from "react-toastify"; -import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx"; import "@smastrom/react-rating/style.css"; +import { queryClient } from "./lib/tanstack-query-client.ts"; +import { QueryClientProvider } from "@tanstack/react-query"; -const TanStackQueryProviderContext = TanStackQueryProvider.getContext(); const router = createRouter({ routeTree, context: { - ...TanStackQueryProviderContext, + queryClient, userQuery: undefined!, }, defaultNotFoundComponent: NotFoundPage, @@ -29,10 +29,10 @@ declare module "@tanstack/react-router" { function App() { return ( - + - + ); } diff --git a/apps/web/src/features/Application/components/ApplicationAgeChart.tsx b/apps/web/src/modules/Application/ApplicationAgeChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/ApplicationAgeChart.tsx rename to apps/web/src/modules/Application/ApplicationAgeChart.tsx diff --git a/apps/web/src/features/Application/components/ApplicationForm.tsx b/apps/web/src/modules/Application/ApplicationForm.tsx similarity index 97% rename from apps/web/src/features/Application/components/ApplicationForm.tsx rename to apps/web/src/modules/Application/ApplicationForm.tsx index 6d84d9e2..ac30a3f3 100644 --- a/apps/web/src/features/Application/components/ApplicationForm.tsx +++ b/apps/web/src/modules/Application/ApplicationForm.tsx @@ -1,14 +1,14 @@ import { useRouter } from "@tanstack/react-router"; -import { build } from "@/features/FormBuilder/build"; +import { build } from "@/modules/FormBuilder/build"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import { showToast } from "@/lib/toast/toast"; import TablerCircleCheck from "~icons/tabler/circle-check"; import { Link } from "react-aria-components"; import TablerArrowLeft from "~icons/tabler/arrow-left"; import { api } from "@/lib/ky"; import { Spinner } from "@/components/ui/Spinner"; -import { useMyApplication } from "@/features/Application/hooks/useMyApplication"; +import { useMyApplication } from "@/modules/Application/hooks/useMyApplication"; import { formatDistanceToNowStrict, parseISO } from "date-fns"; // TODO: can we put these in the assets folder? diff --git a/apps/web/src/features/Application/components/ApplicationGenderChart.tsx b/apps/web/src/modules/Application/ApplicationGenderChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/ApplicationGenderChart.tsx rename to apps/web/src/modules/Application/ApplicationGenderChart.tsx diff --git a/apps/web/src/features/Application/components/ApplicationMajorsChart.tsx b/apps/web/src/modules/Application/ApplicationMajorsChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/ApplicationMajorsChart.tsx rename to apps/web/src/modules/Application/ApplicationMajorsChart.tsx diff --git a/apps/web/src/features/Application/components/ApplicationOverview.tsx b/apps/web/src/modules/Application/ApplicationOverview.tsx similarity index 94% rename from apps/web/src/features/Application/components/ApplicationOverview.tsx rename to apps/web/src/modules/Application/ApplicationOverview.tsx index f04900e1..2533eb49 100644 --- a/apps/web/src/features/Application/components/ApplicationOverview.tsx +++ b/apps/web/src/modules/Application/ApplicationOverview.tsx @@ -1,9 +1,9 @@ import TablerClipboardData from "~icons/tabler/clipboard-data"; -import type { EventOverview } from "@/features/EventOverview/hooks/useEventOverview"; +import type { EventOverview } from "@/modules/EventOverview/hooks/useEventOverview"; import TablerArrowNarrowRight from "~icons/tabler/arrow-narrow-right"; import { Link } from "react-aria-components"; import { useRouter } from "@tanstack/react-router"; -import SubmissionsChart from "@/features/Application/components/SubmissionsChart"; +import SubmissionsChart from "@/modules/Application/components/SubmissionsChart"; interface ApplicationOverviewProps { data: EventOverview; diff --git a/apps/web/src/features/Application/components/ApplicationRaceChart.tsx b/apps/web/src/modules/Application/ApplicationRaceChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/ApplicationRaceChart.tsx rename to apps/web/src/modules/Application/ApplicationRaceChart.tsx diff --git a/apps/web/src/features/Application/components/ApplicationSchoolsChart.tsx b/apps/web/src/modules/Application/ApplicationSchoolsChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/ApplicationSchoolsChart.tsx rename to apps/web/src/modules/Application/ApplicationSchoolsChart.tsx diff --git a/apps/web/src/features/Application/components/ApplicationStatistics.tsx b/apps/web/src/modules/Application/ApplicationStatistics.tsx similarity index 93% rename from apps/web/src/features/Application/components/ApplicationStatistics.tsx rename to apps/web/src/modules/Application/ApplicationStatistics.tsx index 754d0088..ed2be599 100644 --- a/apps/web/src/features/Application/components/ApplicationStatistics.tsx +++ b/apps/web/src/modules/Application/ApplicationStatistics.tsx @@ -4,10 +4,10 @@ import ApplicationRaceChart from "./ApplicationRaceChart"; import ApplicationMajorsChart from "./ApplicationMajorsChart"; import ApplicationSchoolsChart from "./ApplicationSchoolsChart"; import { Heading } from "react-aria-components"; -import { useApplicationStatistics } from "@/features/Application/hooks/useApplicationStatistics"; +import { useApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; import { Card } from "@/components/ui/Card"; -import { useEventOverview } from "@/features/EventOverview/hooks/useEventOverview"; -import ApplicationStats from "./ApplicationStats"; +import { useEventOverview } from "@/modules/EventOverview/hooks/useEventOverview"; +import ApplicationStats from "./components/ApplicationStats"; interface ApplicationStatisticsProps { eventId: string; diff --git a/apps/web/src/features/Application/components/ApplicationStats.tsx b/apps/web/src/modules/Application/ApplicationStats.tsx similarity index 93% rename from apps/web/src/features/Application/components/ApplicationStats.tsx rename to apps/web/src/modules/Application/ApplicationStats.tsx index dc69665f..169a9caa 100644 --- a/apps/web/src/features/Application/components/ApplicationStats.tsx +++ b/apps/web/src/modules/Application/ApplicationStats.tsx @@ -1,8 +1,8 @@ import { DialogTrigger, Link } from "react-aria-components"; -import type { ApplicationStatistics } from "../hooks/useApplicationStatistics"; +import type { ApplicationStatistics } from "./hooks/useApplicationStatistics"; import { Modal } from "@/components/ui/Modal"; import SubmissionsChart from "./SubmissionsChart"; -import type { EventOverview } from "@/features/EventOverview/hooks/useEventOverview"; +import type { EventOverview } from "@/modules/EventOverview/hooks/useEventOverview"; interface ApplicationStatsProps { data: ApplicationStatistics; diff --git a/apps/web/src/features/Application/components/ApplicationStatus.tsx b/apps/web/src/modules/Application/ApplicationStatus.tsx similarity index 97% rename from apps/web/src/features/Application/components/ApplicationStatus.tsx rename to apps/web/src/modules/Application/ApplicationStatus.tsx index bedf5f90..913f4a5b 100644 --- a/apps/web/src/features/Application/components/ApplicationStatus.tsx +++ b/apps/web/src/modules/Application/ApplicationStatus.tsx @@ -1,13 +1,13 @@ import Loading from "@/components/Loading"; import { Button } from "@/components/ui/Button"; -import { useMyApplication } from "@/features/Application/hooks/useMyApplication"; -import { EventBadge } from "@/features/Event/components/EventBadge"; +import { useMyApplication } from "@/modules/Application/hooks/useMyApplication"; +import { EventBadge } from "@/modules/Event/components/EventBadge"; import { Heading } from "react-aria-components"; import TablerUserCode from "~icons/tabler/user-code"; import TablerUsersGroup from "~icons/tabler/users-group"; import TablerDownload from "~icons/tabler/download"; import { api } from "@/lib/ky"; -import { EventButton } from "@/features/Event/components/EventButton"; +import { EventButton } from "@/modules/Event/components/EventButton"; interface ApplicationStatusProps { eventId: string; diff --git a/apps/web/src/features/Application/components/SubmissionsChart.tsx b/apps/web/src/modules/Application/SubmissionsChart.tsx similarity index 100% rename from apps/web/src/features/Application/components/SubmissionsChart.tsx rename to apps/web/src/modules/Application/SubmissionsChart.tsx index c3bfd358..968307bb 100644 --- a/apps/web/src/features/Application/components/SubmissionsChart.tsx +++ b/apps/web/src/modules/Application/SubmissionsChart.tsx @@ -12,11 +12,6 @@ interface SubmissionsChartProps { export default function SubmissionsChart({ submission_stats, }: SubmissionsChartProps) { - // TODO: Make this more elegant, throws error when submission stats is null/undefined - if (!submission_stats) { - return null; - } - const { theme } = useTheme(); const isDark = theme === "dark"; @@ -114,6 +109,11 @@ export default function SubmissionsChart({ [chartData, isDark], ); + // TODO: Make this more elegant, throws error when submission stats is null/undefined + if (!submission_stats) { + return null; + } + return (
diff --git a/apps/web/src/features/Application/components/bell.svg b/apps/web/src/modules/Application/bell.svg similarity index 100% rename from apps/web/src/features/Application/components/bell.svg rename to apps/web/src/modules/Application/bell.svg diff --git a/apps/web/src/features/Application/components/cloud.svg b/apps/web/src/modules/Application/cloud.svg similarity index 100% rename from apps/web/src/features/Application/components/cloud.svg rename to apps/web/src/modules/Application/cloud.svg diff --git a/apps/web/src/features/Application/components/cloud2.svg b/apps/web/src/modules/Application/cloud2.svg similarity index 100% rename from apps/web/src/features/Application/components/cloud2.svg rename to apps/web/src/modules/Application/cloud2.svg diff --git a/apps/web/src/features/Application/components/cloud3.svg b/apps/web/src/modules/Application/cloud3.svg similarity index 100% rename from apps/web/src/features/Application/components/cloud3.svg rename to apps/web/src/modules/Application/cloud3.svg diff --git a/apps/web/src/features/Application/components/cloud4.svg b/apps/web/src/modules/Application/cloud4.svg similarity index 100% rename from apps/web/src/features/Application/components/cloud4.svg rename to apps/web/src/modules/Application/cloud4.svg diff --git a/apps/web/src/features/Application/hooks/useApplication.ts b/apps/web/src/modules/Application/hooks/useApplication.ts similarity index 100% rename from apps/web/src/features/Application/hooks/useApplication.ts rename to apps/web/src/modules/Application/hooks/useApplication.ts diff --git a/apps/web/src/features/Application/hooks/useApplicationStatistics.ts b/apps/web/src/modules/Application/hooks/useApplicationStatistics.ts similarity index 100% rename from apps/web/src/features/Application/hooks/useApplicationStatistics.ts rename to apps/web/src/modules/Application/hooks/useApplicationStatistics.ts diff --git a/apps/web/src/features/Application/hooks/useAssignedApplication.ts b/apps/web/src/modules/Application/hooks/useAssignedApplication.ts similarity index 100% rename from apps/web/src/features/Application/hooks/useAssignedApplication.ts rename to apps/web/src/modules/Application/hooks/useAssignedApplication.ts diff --git a/apps/web/src/features/Application/hooks/useMyApplication.ts b/apps/web/src/modules/Application/hooks/useMyApplication.ts similarity index 100% rename from apps/web/src/features/Application/hooks/useMyApplication.ts rename to apps/web/src/modules/Application/hooks/useMyApplication.ts diff --git a/apps/web/src/features/Application/components/tower.svg b/apps/web/src/modules/Application/tower.svg similarity index 100% rename from apps/web/src/features/Application/components/tower.svg rename to apps/web/src/modules/Application/tower.svg diff --git a/apps/web/src/features/ApplicationReview/components/ResetReviewModal.tsx b/apps/web/src/modules/ApplicationReview/ResetReviewModal.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/ResetReviewModal.tsx rename to apps/web/src/modules/ApplicationReview/ResetReviewModal.tsx diff --git a/apps/web/src/features/ApplicationReview/components/Review/ApplicationReviewContainer.tsx b/apps/web/src/modules/ApplicationReview/Review/ApplicationReviewContainer.tsx similarity index 97% rename from apps/web/src/features/ApplicationReview/components/Review/ApplicationReviewContainer.tsx rename to apps/web/src/modules/ApplicationReview/Review/ApplicationReviewContainer.tsx index 10ec7b81..1f16b347 100644 --- a/apps/web/src/features/ApplicationReview/components/Review/ApplicationReviewContainer.tsx +++ b/apps/web/src/modules/ApplicationReview/Review/ApplicationReviewContainer.tsx @@ -1,8 +1,8 @@ import TablerLoader from "~icons/tabler/loader-2"; -import { useApplication } from "@/features/Application/hooks/useApplication"; +import { useApplication } from "@/modules/Application/hooks/useApplication"; import EssayResponse from "./EssayResponse"; import { RatingFields } from "./RatingFields"; -import type { AssignedApplications } from "@/features/Application/hooks/useAssignedApplication"; +import type { AssignedApplications } from "@/modules/Application/hooks/useAssignedApplication"; import { useApplicationResume } from "../../hooks/useAppResume"; import { useRatings } from "../../hooks/useRatings"; import { ReviewNavigation } from "./ReviewNavigation"; diff --git a/apps/web/src/features/ApplicationReview/components/Review/ApplicationReviewPage.tsx b/apps/web/src/modules/ApplicationReview/Review/ApplicationReviewPage.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/Review/ApplicationReviewPage.tsx rename to apps/web/src/modules/ApplicationReview/Review/ApplicationReviewPage.tsx diff --git a/apps/web/src/features/ApplicationReview/components/Review/EssayResponse.tsx b/apps/web/src/modules/ApplicationReview/Review/EssayResponse.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/Review/EssayResponse.tsx rename to apps/web/src/modules/ApplicationReview/Review/EssayResponse.tsx diff --git a/apps/web/src/features/ApplicationReview/components/Review/RatingFields.tsx b/apps/web/src/modules/ApplicationReview/Review/RatingFields.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/Review/RatingFields.tsx rename to apps/web/src/modules/ApplicationReview/Review/RatingFields.tsx diff --git a/apps/web/src/features/ApplicationReview/components/Review/ReviewNavigation.tsx b/apps/web/src/modules/ApplicationReview/Review/ReviewNavigation.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/Review/ReviewNavigation.tsx rename to apps/web/src/modules/ApplicationReview/Review/ReviewNavigation.tsx diff --git a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewNotStarted.tsx b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewNotStarted.tsx similarity index 76% rename from apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewNotStarted.tsx rename to apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewNotStarted.tsx index f1e8c90f..5d5f163b 100644 --- a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewNotStarted.tsx +++ b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewNotStarted.tsx @@ -1,7 +1,7 @@ -import type { Event } from "@/features/Event/schemas/event"; +import type { Event } from "@/modules/Event/schemas/event"; import StartReviewButton from "./StartReviewButton"; -import { useApplicationStatistics } from "@/features/Application/hooks/useApplicationStatistics"; -import { useEventStaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import { useApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; +import { useEventStaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; interface Props { event: Event; diff --git a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerAssignmentModal.tsx b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerAssignmentModal.tsx similarity index 91% rename from apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerAssignmentModal.tsx rename to apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerAssignmentModal.tsx index 4199e96e..b6d94469 100644 --- a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerAssignmentModal.tsx +++ b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerAssignmentModal.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; -import type { Event } from "@/features/Event/schemas/event"; -import type { ApplicationStatistics } from "@/features/Application/hooks/useApplicationStatistics"; -import type { StaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import type { Event } from "@/modules/Event/schemas/event"; +import type { ApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; +import type { StaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; import ReviewerList from "./ReviewerList"; import SummaryFooter from "./SummaryFooter"; import { useAppReviewAdminActions } from "../../hooks/useAppReviewAdminActions"; diff --git a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerList.tsx b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerList.tsx similarity index 96% rename from apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerList.tsx rename to apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerList.tsx index 92464532..29929c43 100644 --- a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/ReviewerList.tsx +++ b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/ReviewerList.tsx @@ -2,7 +2,7 @@ import { Checkbox, CheckboxGroup } from "@/components/ui/Checkbox"; import { NumberField, Input } from "react-aria-components"; import { Tooltip } from "@/components/ui/Tooltip"; import TablerHelpCircle from "~icons/tabler/help-circle"; -import type { StaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import type { StaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; import type { AssignedReviewer } from "./ReviewerAssignmentModal"; import type { Dispatch, SetStateAction } from "react"; import { Button } from "@/components/ui/Button"; diff --git a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/StartReviewButton.tsx b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/StartReviewButton.tsx similarity index 83% rename from apps/web/src/features/ApplicationReview/components/ReviewNotStarted/StartReviewButton.tsx rename to apps/web/src/modules/ApplicationReview/ReviewNotStarted/StartReviewButton.tsx index 8cf13762..656c1ab3 100644 --- a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/StartReviewButton.tsx +++ b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/StartReviewButton.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { Button } from "@/components/ui/Button"; import { DialogTrigger } from "react-aria-components"; import ReviewerAssignmentModal from "./ReviewerAssignmentModal"; -import type { Event } from "@/features/Event/schemas/event"; -import type { ApplicationStatistics } from "@/features/Application/hooks/useApplicationStatistics"; -import type { StaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import type { Event } from "@/modules/Event/schemas/event"; +import type { ApplicationStatistics } from "@/modules/Application/hooks/useApplicationStatistics"; +import type { StaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; interface Props { event: Event; diff --git a/apps/web/src/features/ApplicationReview/components/ReviewNotStarted/SummaryFooter.tsx b/apps/web/src/modules/ApplicationReview/ReviewNotStarted/SummaryFooter.tsx similarity index 100% rename from apps/web/src/features/ApplicationReview/components/ReviewNotStarted/SummaryFooter.tsx rename to apps/web/src/modules/ApplicationReview/ReviewNotStarted/SummaryFooter.tsx diff --git a/apps/web/src/features/ApplicationReview/hooks/useAppResume.ts b/apps/web/src/modules/ApplicationReview/hooks/useAppResume.ts similarity index 100% rename from apps/web/src/features/ApplicationReview/hooks/useAppResume.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAppResume.ts diff --git a/apps/web/src/features/ApplicationReview/hooks/useAppReviewActions.ts b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewActions.ts similarity index 92% rename from apps/web/src/features/ApplicationReview/hooks/useAppReviewActions.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAppReviewActions.ts index 07262f42..f423955d 100644 --- a/apps/web/src/features/ApplicationReview/hooks/useAppReviewActions.ts +++ b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewActions.ts @@ -1,11 +1,11 @@ -import { type AssignedApplications } from "@/features/Application/hooks/useAssignedApplication"; +import { type AssignedApplications } from "@/modules/Application/hooks/useAssignedApplication"; import { api } from "@/lib/ky"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { getAssignedApplicationsQueryKey } from "./useAssignedApplications"; import { type Application, getApplicationQueryKey, -} from "@/features/Application/hooks/useApplication"; +} from "@/modules/Application/hooks/useApplication"; const submitReview = async ( eventId: string, diff --git a/apps/web/src/features/ApplicationReview/hooks/useAppReviewAdminActions.ts b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewAdminActions.ts similarity index 96% rename from apps/web/src/features/ApplicationReview/hooks/useAppReviewAdminActions.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAppReviewAdminActions.ts index e5ffa975..bd9dab00 100644 --- a/apps/web/src/features/ApplicationReview/hooks/useAppReviewAdminActions.ts +++ b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewAdminActions.ts @@ -1,7 +1,7 @@ import { api } from "@/lib/ky"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { AssignedReviewer } from "../components/ReviewNotStarted/ReviewerAssignmentModal"; -import { getEventQueryKey } from "@/features/Event/hooks/useEvent"; +import { getEventQueryKey } from "@/modules/Event/hooks/useEvent"; import { getAssignedApplicationsQueryKey } from "./useAssignedApplications"; const assignReviewers = async ( diff --git a/apps/web/src/features/ApplicationReview/hooks/useAppReviewProgress.ts b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewProgress.ts similarity index 100% rename from apps/web/src/features/ApplicationReview/hooks/useAppReviewProgress.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAppReviewProgress.ts diff --git a/apps/web/src/features/ApplicationReview/hooks/useAppReviewTutorial.ts b/apps/web/src/modules/ApplicationReview/hooks/useAppReviewTutorial.ts similarity index 100% rename from apps/web/src/features/ApplicationReview/hooks/useAppReviewTutorial.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAppReviewTutorial.ts diff --git a/apps/web/src/features/ApplicationReview/hooks/useAssignedApplications.ts b/apps/web/src/modules/ApplicationReview/hooks/useAssignedApplications.ts similarity index 100% rename from apps/web/src/features/ApplicationReview/hooks/useAssignedApplications.ts rename to apps/web/src/modules/ApplicationReview/hooks/useAssignedApplications.ts diff --git a/apps/web/src/features/ApplicationReview/hooks/useRatings.ts b/apps/web/src/modules/ApplicationReview/hooks/useRatings.ts similarity index 100% rename from apps/web/src/features/ApplicationReview/hooks/useRatings.ts rename to apps/web/src/modules/ApplicationReview/hooks/useRatings.ts diff --git a/apps/web/src/features/Auth/components/Login.tsx b/apps/web/src/modules/Auth/Login.tsx similarity index 100% rename from apps/web/src/features/Auth/components/Login.tsx rename to apps/web/src/modules/Auth/Login.tsx diff --git a/apps/web/src/features/Auth/hooks/useAuth.ts b/apps/web/src/modules/Auth/hooks/useAuth.ts similarity index 95% rename from apps/web/src/features/Auth/hooks/useAuth.ts rename to apps/web/src/modules/Auth/hooks/useAuth.ts index dd96b4df..415ffcf9 100644 --- a/apps/web/src/features/Auth/hooks/useAuth.ts +++ b/apps/web/src/modules/Auth/hooks/useAuth.ts @@ -1,7 +1,7 @@ // import Cookies from "js-cookie"; // import { useEffect, useRef } from "react"; // import { APP_URL } from "@/utils/url"; -// import type { User } from "@/features/Auth/types"; +// import type { User } from "@/modules/Auth/types"; // export const useAuth = () => { // // // https://github.com/TanStack/router/discussions/1668#discussioncomment-10634735 diff --git a/apps/web/src/features/Auth/types.ts b/apps/web/src/modules/Auth/types.ts similarity index 100% rename from apps/web/src/features/Auth/types.ts rename to apps/web/src/modules/Auth/types.ts diff --git a/apps/web/src/features/CheckIn/components/CheckInBadge.tsx b/apps/web/src/modules/CheckIn/components/CheckInBadge.tsx similarity index 100% rename from apps/web/src/features/CheckIn/components/CheckInBadge.tsx rename to apps/web/src/modules/CheckIn/components/CheckInBadge.tsx diff --git a/apps/web/src/features/CheckIn/components/CheckInModal.tsx b/apps/web/src/modules/CheckIn/components/CheckInModal.tsx similarity index 98% rename from apps/web/src/features/CheckIn/components/CheckInModal.tsx rename to apps/web/src/modules/CheckIn/components/CheckInModal.tsx index f60d6830..9d2db2dd 100644 --- a/apps/web/src/features/CheckIn/components/CheckInModal.tsx +++ b/apps/web/src/modules/CheckIn/components/CheckInModal.tsx @@ -4,7 +4,7 @@ import { CheckInBadge } from "./CheckInBadge"; import { Button } from "@/components/ui/Button"; import { useState } from "react"; import { useUserEventInfo } from "../hooks/useUserEventInfo"; -import RoleBadge from "@/features/EventAdmin/components/RoleBadge"; +import RoleBadge from "@/modules/EventAdmin/components/RoleBadge"; import TablerCheck from "~icons/tabler/check"; import TablerX from "~icons/tabler/x"; import { toast } from "react-toastify"; diff --git a/apps/web/src/features/CheckIn/hooks/useUserEventInfo.ts b/apps/web/src/modules/CheckIn/hooks/useUserEventInfo.ts similarity index 100% rename from apps/web/src/features/CheckIn/hooks/useUserEventInfo.ts rename to apps/web/src/modules/CheckIn/hooks/useUserEventInfo.ts diff --git a/apps/web/src/features/Dashboard/components/ApplicantAppShell.tsx b/apps/web/src/modules/Dashboard/ApplicantAppShell.tsx similarity index 94% rename from apps/web/src/features/Dashboard/components/ApplicantAppShell.tsx rename to apps/web/src/modules/Dashboard/ApplicantAppShell.tsx index 54eaeeeb..e1ddf49b 100644 --- a/apps/web/src/features/Dashboard/components/ApplicantAppShell.tsx +++ b/apps/web/src/modules/Dashboard/ApplicantAppShell.tsx @@ -21,8 +21,9 @@ export default function ApplicantAppShell({ const applicationStatusActive = /^\/events\/[^/]+\/dashboard\/application-status\/?$/.test(pathname); - const teamFormationActive = - /^\/events\/[^/]+\/dashboard\/my-team\/?$/.test(pathname); + const teamFormationActive = /^\/events\/[^/]+\/dashboard\/my-team\/?$/.test( + pathname, + ); return ( diff --git a/apps/web/src/features/Dashboard/components/AttendeeAppShell.tsx b/apps/web/src/modules/Dashboard/AttendeeAppShell.tsx similarity index 100% rename from apps/web/src/features/Dashboard/components/AttendeeAppShell.tsx rename to apps/web/src/modules/Dashboard/AttendeeAppShell.tsx diff --git a/apps/web/src/features/Dashboard/components/StaffAppShell.tsx b/apps/web/src/modules/Dashboard/StaffAppShell.tsx similarity index 100% rename from apps/web/src/features/Dashboard/components/StaffAppShell.tsx rename to apps/web/src/modules/Dashboard/StaffAppShell.tsx diff --git a/apps/web/src/features/Event/api/getEvent.ts b/apps/web/src/modules/Event/api/getEvent.ts similarity index 100% rename from apps/web/src/features/Event/api/getEvent.ts rename to apps/web/src/modules/Event/api/getEvent.ts diff --git a/apps/web/src/features/Event/api/getUserEventRole.ts b/apps/web/src/modules/Event/api/getUserEventRole.ts similarity index 100% rename from apps/web/src/features/Event/api/getUserEventRole.ts rename to apps/web/src/modules/Event/api/getUserEventRole.ts diff --git a/apps/web/src/features/Event/api/updateEvent.ts b/apps/web/src/modules/Event/api/updateEvent.ts similarity index 100% rename from apps/web/src/features/Event/api/updateEvent.ts rename to apps/web/src/modules/Event/api/updateEvent.ts diff --git a/apps/web/src/features/Event/applicationStatus.ts b/apps/web/src/modules/Event/applicationStatus.ts similarity index 100% rename from apps/web/src/features/Event/applicationStatus.ts rename to apps/web/src/modules/Event/applicationStatus.ts diff --git a/apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx b/apps/web/src/modules/Event/components/EventAcceptanceWithdrawalModal.tsx similarity index 96% rename from apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx rename to apps/web/src/modules/Event/components/EventAcceptanceWithdrawalModal.tsx index 6b8b540e..08c45a1e 100644 --- a/apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx +++ b/apps/web/src/modules/Event/components/EventAcceptanceWithdrawalModal.tsx @@ -5,7 +5,7 @@ import { api } from "@/lib/ky"; import { showToast } from "@/lib/toast/toast"; import { useQueryClient } from "@tanstack/react-query"; import { eventsQueryKey } from "../hooks/useEventsWithUserInfo"; -import { myApplicationBaseKey } from "@/features/Application/hooks/useMyApplication"; +import { myApplicationBaseKey } from "@/modules/Application/hooks/useMyApplication"; interface EventAcceptanceWithdrawalModalProps { eventId: string; diff --git a/apps/web/src/features/Event/components/EventAttendanceWithdrawalModal.tsx b/apps/web/src/modules/Event/components/EventAttendanceWithdrawalModal.tsx similarity index 96% rename from apps/web/src/features/Event/components/EventAttendanceWithdrawalModal.tsx rename to apps/web/src/modules/Event/components/EventAttendanceWithdrawalModal.tsx index de71ec3f..f527ad13 100644 --- a/apps/web/src/features/Event/components/EventAttendanceWithdrawalModal.tsx +++ b/apps/web/src/modules/Event/components/EventAttendanceWithdrawalModal.tsx @@ -5,7 +5,7 @@ import { api } from "@/lib/ky"; import { showToast } from "@/lib/toast/toast"; import { useQueryClient } from "@tanstack/react-query"; import { eventsQueryKey } from "../hooks/useEventsWithUserInfo"; -import { myApplicationBaseKey } from "@/features/Application/hooks/useMyApplication"; +import { myApplicationBaseKey } from "@/modules/Application/hooks/useMyApplication"; import { useNavigate } from "@tanstack/react-router"; interface EventAttendanceWithdrawalModalProps { diff --git a/apps/web/src/features/Event/components/EventBadge.tsx b/apps/web/src/modules/Event/components/EventBadge.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventBadge.tsx rename to apps/web/src/modules/Event/components/EventBadge.tsx diff --git a/apps/web/src/features/Event/components/EventBannerUploader.tsx b/apps/web/src/modules/Event/components/EventBannerUploader.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventBannerUploader.tsx rename to apps/web/src/modules/Event/components/EventBannerUploader.tsx diff --git a/apps/web/src/features/Event/components/EventButton.tsx b/apps/web/src/modules/Event/components/EventButton.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventButton.tsx rename to apps/web/src/modules/Event/components/EventButton.tsx diff --git a/apps/web/src/features/Event/components/EventCard.tsx b/apps/web/src/modules/Event/components/EventCard.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventCard.tsx rename to apps/web/src/modules/Event/components/EventCard.tsx diff --git a/apps/web/src/features/Event/components/EventDetailsModal.tsx b/apps/web/src/modules/Event/components/EventDetailsModal.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventDetailsModal.tsx rename to apps/web/src/modules/Event/components/EventDetailsModal.tsx diff --git a/apps/web/src/features/Event/components/EventSettingsForm.tsx b/apps/web/src/modules/Event/components/EventSettingsForm.tsx similarity index 100% rename from apps/web/src/features/Event/components/EventSettingsForm.tsx rename to apps/web/src/modules/Event/components/EventSettingsForm.tsx diff --git a/apps/web/src/features/Event/components/EventWaitlistModal.tsx b/apps/web/src/modules/Event/components/EventWaitlistModal.tsx similarity index 96% rename from apps/web/src/features/Event/components/EventWaitlistModal.tsx rename to apps/web/src/modules/Event/components/EventWaitlistModal.tsx index a41ea89d..3930be01 100644 --- a/apps/web/src/features/Event/components/EventWaitlistModal.tsx +++ b/apps/web/src/modules/Event/components/EventWaitlistModal.tsx @@ -5,7 +5,7 @@ import { api } from "@/lib/ky"; import { showToast } from "@/lib/toast/toast"; import { useQueryClient } from "@tanstack/react-query"; import { eventsQueryKey } from "../hooks/useEventsWithUserInfo"; -import { myApplicationBaseKey } from "@/features/Application/hooks/useMyApplication"; +import { myApplicationBaseKey } from "@/modules/Application/hooks/useMyApplication"; interface EventWaitlistModalProps { eventId: string; diff --git a/apps/web/src/features/Event/components/placeholder.jpg b/apps/web/src/modules/Event/components/placeholder.jpg similarity index 100% rename from apps/web/src/features/Event/components/placeholder.jpg rename to apps/web/src/modules/Event/components/placeholder.jpg diff --git a/apps/web/src/features/Event/components/stories/EventBadge.stories.tsx b/apps/web/src/modules/Event/components/stories/EventBadge.stories.tsx similarity index 100% rename from apps/web/src/features/Event/components/stories/EventBadge.stories.tsx rename to apps/web/src/modules/Event/components/stories/EventBadge.stories.tsx diff --git a/apps/web/src/features/Event/components/stories/EventButton.stories.tsx b/apps/web/src/modules/Event/components/stories/EventButton.stories.tsx similarity index 100% rename from apps/web/src/features/Event/components/stories/EventButton.stories.tsx rename to apps/web/src/modules/Event/components/stories/EventButton.stories.tsx diff --git a/apps/web/src/features/Event/components/stories/EventCard.stories.tsx b/apps/web/src/modules/Event/components/stories/EventCard.stories.tsx similarity index 100% rename from apps/web/src/features/Event/components/stories/EventCard.stories.tsx rename to apps/web/src/modules/Event/components/stories/EventCard.stories.tsx diff --git a/apps/web/src/features/Event/hooks/useEvent.ts b/apps/web/src/modules/Event/hooks/useEvent.ts similarity index 100% rename from apps/web/src/features/Event/hooks/useEvent.ts rename to apps/web/src/modules/Event/hooks/useEvent.ts diff --git a/apps/web/src/features/Event/hooks/useEventBannerActions.ts b/apps/web/src/modules/Event/hooks/useEventBannerActions.ts similarity index 100% rename from apps/web/src/features/Event/hooks/useEventBannerActions.ts rename to apps/web/src/modules/Event/hooks/useEventBannerActions.ts diff --git a/apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts b/apps/web/src/modules/Event/hooks/useEventsWithUserInfo.ts similarity index 100% rename from apps/web/src/features/Event/hooks/useEventsWithUserInfo.ts rename to apps/web/src/modules/Event/hooks/useEventsWithUserInfo.ts diff --git a/apps/web/src/features/Event/hooks/useUpdateEvent.ts b/apps/web/src/modules/Event/hooks/useUpdateEvent.ts similarity index 100% rename from apps/web/src/features/Event/hooks/useUpdateEvent.ts rename to apps/web/src/modules/Event/hooks/useUpdateEvent.ts diff --git a/apps/web/src/features/Event/hooks/useUpdateEventForm.tsx b/apps/web/src/modules/Event/hooks/useUpdateEventForm.tsx similarity index 100% rename from apps/web/src/features/Event/hooks/useUpdateEventForm.tsx rename to apps/web/src/modules/Event/hooks/useUpdateEventForm.tsx diff --git a/apps/web/src/features/Event/schemas/event.ts b/apps/web/src/modules/Event/schemas/event.ts similarity index 100% rename from apps/web/src/features/Event/schemas/event.ts rename to apps/web/src/modules/Event/schemas/event.ts diff --git a/apps/web/src/features/Event/utils/mapper.ts b/apps/web/src/modules/Event/utils/mapper.ts similarity index 100% rename from apps/web/src/features/Event/utils/mapper.ts rename to apps/web/src/modules/Event/utils/mapper.ts diff --git a/apps/web/src/features/EventAdmin/components/AddStaffModal.tsx b/apps/web/src/modules/EventAdmin/AddStaffModal.tsx similarity index 95% rename from apps/web/src/features/EventAdmin/components/AddStaffModal.tsx rename to apps/web/src/modules/EventAdmin/AddStaffModal.tsx index bf9467a6..aab5bc1c 100644 --- a/apps/web/src/features/EventAdmin/components/AddStaffModal.tsx +++ b/apps/web/src/modules/EventAdmin/AddStaffModal.tsx @@ -1,13 +1,13 @@ import { Button } from "@/components/ui/Button"; import { Modal } from "@/components/ui/Modal"; import { TextField } from "@/components/ui/TextField"; -import { useEventStaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; -import { useUsers } from "@/features/Users/hooks/useUsers"; +import { useEventStaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import { useUsers } from "@/modules/Users/hooks/useUsers"; import { useContext, useMemo, useState } from "react"; import debounce from "lodash.debounce"; import { cn } from "@/utils/cn"; import type { User } from "@/lib/openapi/types"; -import { useStaffActions } from "../hooks/useStaffActions"; +import { useStaffActions } from "./hooks/useStaffActions"; import { toast } from "react-toastify"; import { OverlayTriggerStateContext } from "react-aria-components"; diff --git a/apps/web/src/features/EventAdmin/components/DeleteStaffDialog.tsx b/apps/web/src/modules/EventAdmin/DeleteStaffDialog.tsx similarity index 92% rename from apps/web/src/features/EventAdmin/components/DeleteStaffDialog.tsx rename to apps/web/src/modules/EventAdmin/DeleteStaffDialog.tsx index a9df2d13..7b27b556 100644 --- a/apps/web/src/features/EventAdmin/components/DeleteStaffDialog.tsx +++ b/apps/web/src/modules/EventAdmin/DeleteStaffDialog.tsx @@ -7,8 +7,8 @@ import { Text, } from "react-aria-components"; import { useContext } from "react"; -import type { StaffUser } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; -import { useStaffActions } from "../hooks/useStaffActions"; +import type { StaffUser } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import { useStaffActions } from "./hooks/useStaffActions"; import { toast } from "react-toastify"; interface DeleteStaffDialogProps { diff --git a/apps/web/src/features/EventAdmin/components/RoleBadge.tsx b/apps/web/src/modules/EventAdmin/RoleBadge.tsx similarity index 100% rename from apps/web/src/features/EventAdmin/components/RoleBadge.tsx rename to apps/web/src/modules/EventAdmin/RoleBadge.tsx diff --git a/apps/web/src/features/EventAdmin/components/StaffTable.tsx b/apps/web/src/modules/EventAdmin/StaffTable.tsx similarity index 97% rename from apps/web/src/features/EventAdmin/components/StaffTable.tsx rename to apps/web/src/modules/EventAdmin/StaffTable.tsx index ccaa105a..4445f703 100644 --- a/apps/web/src/features/EventAdmin/components/StaffTable.tsx +++ b/apps/web/src/modules/EventAdmin/StaffTable.tsx @@ -10,7 +10,7 @@ import { type FilterFn, } from "@tanstack/react-table"; import RoleBadge from "./RoleBadge"; -import type { StaffUser } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import type { StaffUser } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; import { TextField } from "@/components/ui/TextField"; import { useMemo } from "react"; import { DialogTrigger } from "react-aria-components"; diff --git a/apps/web/src/features/EventAdmin/components/UserSideDrawer.tsx b/apps/web/src/modules/EventAdmin/UserSideDrawer.tsx similarity index 77% rename from apps/web/src/features/EventAdmin/components/UserSideDrawer.tsx rename to apps/web/src/modules/EventAdmin/UserSideDrawer.tsx index 008e8b73..618c8c3d 100644 --- a/apps/web/src/features/EventAdmin/components/UserSideDrawer.tsx +++ b/apps/web/src/modules/EventAdmin/UserSideDrawer.tsx @@ -1,5 +1,5 @@ import { Modal } from "@/components/ui/Modal"; -import type { StaffUser } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import type { StaffUser } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; interface UserSideDrawerProps { user: StaffUser; diff --git a/apps/web/src/features/EventAdmin/components/UserTable.tsx b/apps/web/src/modules/EventAdmin/UserTable.tsx similarity index 96% rename from apps/web/src/features/EventAdmin/components/UserTable.tsx rename to apps/web/src/modules/EventAdmin/UserTable.tsx index c26082a1..ea765cf2 100644 --- a/apps/web/src/features/EventAdmin/components/UserTable.tsx +++ b/apps/web/src/modules/EventAdmin/UserTable.tsx @@ -5,12 +5,12 @@ import { useMemo } from "react"; import { DialogTrigger, TooltipTrigger, Tooltip } from "react-aria-components"; import RoleBadge from "./RoleBadge"; -import type { EventUser } from "@/features/PlatformAdmin/EventManager/hooks/useEventUsers"; +import type { EventUser } from "@/modules/PlatformAdmin/EventManager/hooks/useEventUsers"; import { UserSideDrawer } from "./UserSideDrawer"; import { Route as EventUsersRoute } from "@/routes/_protected/events/$eventId/dashboard/_admin/user-management"; import { Table } from "@/components/ui/Table"; -import { useUrlTableState } from "../hooks/useUrlTableState"; +import { useUrlTableState } from "./hooks/useUrlTableState"; // Warning: When using the url table state saving (useUrlTableState hook), random query parameters may be interpreted as table filters if column name is identical. diff --git a/apps/web/src/features/EventAdmin/hooks/useStaffActions.ts b/apps/web/src/modules/EventAdmin/hooks/useStaffActions.ts similarity index 95% rename from apps/web/src/features/EventAdmin/hooks/useStaffActions.ts rename to apps/web/src/modules/EventAdmin/hooks/useStaffActions.ts index f0eb94ff..3ec10b28 100644 --- a/apps/web/src/features/EventAdmin/hooks/useStaffActions.ts +++ b/apps/web/src/modules/EventAdmin/hooks/useStaffActions.ts @@ -1,7 +1,7 @@ import { getEventStaffUsersQueryKey, type StaffUsers, -} from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +} from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; import { api } from "@/lib/ky"; import type { components } from "@/lib/openapi/schema"; import { useMutation, useQueryClient } from "@tanstack/react-query"; diff --git a/apps/web/src/features/EventAdmin/hooks/useUrlTableState.ts b/apps/web/src/modules/EventAdmin/hooks/useUrlTableState.ts similarity index 100% rename from apps/web/src/features/EventAdmin/hooks/useUrlTableState.ts rename to apps/web/src/modules/EventAdmin/hooks/useUrlTableState.ts diff --git a/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx b/apps/web/src/modules/EventOverview/AttendeeOverview.tsx similarity index 98% rename from apps/web/src/features/EventOverview/components/AttendeeOverview.tsx rename to apps/web/src/modules/EventOverview/AttendeeOverview.tsx index 69d5b4d3..bdd228d0 100644 --- a/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx +++ b/apps/web/src/modules/EventOverview/AttendeeOverview.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; -import { EventAttendanceWithdrawalModal } from "@/features/Event/components/EventAttendanceWithdrawalModal"; +import { EventAttendanceWithdrawalModal } from "@/modules/Event/components/EventAttendanceWithdrawalModal"; import { generateIdentifyIntent } from "@/lib/qr-intents/generate"; import { DialogTrigger, Heading, Link } from "react-aria-components"; import QRCode from "react-qr-code"; diff --git a/apps/web/src/features/EventOverview/components/EventDetails.tsx b/apps/web/src/modules/EventOverview/EventDetails.tsx similarity index 98% rename from apps/web/src/features/EventOverview/components/EventDetails.tsx rename to apps/web/src/modules/EventOverview/EventDetails.tsx index 4ddacfbe..8e2a3a84 100644 --- a/apps/web/src/features/EventOverview/components/EventDetails.tsx +++ b/apps/web/src/modules/EventOverview/EventDetails.tsx @@ -6,7 +6,7 @@ import TablerUsers from "~icons/tabler/users"; import TablerMapPin from "~icons/tabler/map-pin"; import { useEffect, useState } from "react"; import { differenceInDays, type Duration, intervalToDuration } from "date-fns"; -import type { EventOverview } from "@/features/EventOverview/hooks/useEventOverview"; +import type { EventOverview } from "@/modules/EventOverview/hooks/useEventOverview"; interface EventDetailsProps { data: EventOverview; diff --git a/apps/web/src/features/EventOverview/components/StaffOverview.tsx b/apps/web/src/modules/EventOverview/StaffOverview.tsx similarity index 85% rename from apps/web/src/features/EventOverview/components/StaffOverview.tsx rename to apps/web/src/modules/EventOverview/StaffOverview.tsx index d29f621d..1bcdc977 100644 --- a/apps/web/src/features/EventOverview/components/StaffOverview.tsx +++ b/apps/web/src/modules/EventOverview/StaffOverview.tsx @@ -1,8 +1,8 @@ import { Heading } from "react-aria-components"; import { Card } from "@/components/ui/Card"; -import ApplicationOverview from "@/features/Application/components/ApplicationOverview"; -import EventDetails from "@/features/EventOverview/components/EventDetails"; -import { useEventOverview } from "@/features/EventOverview/hooks/useEventOverview"; +import ApplicationOverview from "@/modules/Application/components/ApplicationOverview"; +import EventDetails from "@/modules/EventOverview/components/EventDetails"; +import { useEventOverview } from "@/modules/EventOverview/hooks/useEventOverview"; interface Props { eventId: string; diff --git a/apps/web/src/features/EventOverview/hooks/useEventOverview.ts b/apps/web/src/modules/EventOverview/hooks/useEventOverview.ts similarity index 100% rename from apps/web/src/features/EventOverview/hooks/useEventOverview.ts rename to apps/web/src/modules/EventOverview/hooks/useEventOverview.ts diff --git a/apps/web/src/features/FormBuilder/build.tsx b/apps/web/src/modules/FormBuilder/build.tsx similarity index 95% rename from apps/web/src/features/FormBuilder/build.tsx rename to apps/web/src/modules/FormBuilder/build.tsx index 8e657646..2ca8a942 100644 --- a/apps/web/src/features/FormBuilder/build.tsx +++ b/apps/web/src/modules/FormBuilder/build.tsx @@ -7,7 +7,7 @@ import { type FormObject, type FormQuestionItemSchemaType, FormSchema, -} from "@/features/FormBuilder/formSchema"; +} from "@/modules/FormBuilder/formSchema"; import { memo, useEffect, @@ -17,8 +17,8 @@ import { type ReactNode, } from "react"; import z from "zod"; -import { QuestionTypes } from "@/features/FormBuilder/types"; -import { textFieldIcons } from "@/features/FormBuilder/icons"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; +import { textFieldIcons } from "@/modules/FormBuilder/icons"; import { useStore } from "@tanstack/react-form"; import { questionTypeItemMap } from "./questions/createQuestionItem"; import { parseDate } from "@internationalized/date"; @@ -521,28 +521,23 @@ function useJSONData( if (typeof item.options === "object" && "data" in item.options) { const data = item.options.data; - switch (data) { - case "schools": - const schoolsRes = await fetch(`/assets/schools.json`); - const schools = await schoolsRes.json(); - setData( - schools.map((item: string) => ({ id: item, name: item })), - ); - break; - case "majors": - const majorsRes = await fetch(`/assets/majors.json`); - const majors = await majorsRes.json(); - setData(majors.map((item: string) => ({ id: item, name: item }))); - break; - case "countries": - const countriesRes = await fetch(`/assets/countries.json`); - const countries = await countriesRes.json(); - setData( - countries.map(({ name, code }: { name: string, code: string }) => ({ id: code, name })), - ); - break; - default: - break; + if (data === "schools") { + const schoolsRes = await fetch(`/assets/schools.json`); + const schools = await schoolsRes.json(); + setData(schools.map((item: string) => ({ id: item, name: item }))); + } else if (data === "majors") { + const majorsRes = await fetch(`/assets/majors.json`); + const majors = await majorsRes.json(); + setData(majors.map((item: string) => ({ id: item, name: item }))); + } else if (data === "countries") { + const countriesRes = await fetch(`/assets/countries.json`); + const countries = await countriesRes.json(); + setData( + countries.map(({ name, code }: { name: string; code: string }) => ({ + id: code, + name, + })), + ); } } } diff --git a/apps/web/src/features/FormBuilder/errorMessage.ts b/apps/web/src/modules/FormBuilder/errorMessage.ts similarity index 100% rename from apps/web/src/features/FormBuilder/errorMessage.ts rename to apps/web/src/modules/FormBuilder/errorMessage.ts diff --git a/apps/web/src/features/FormBuilder/formSchema.ts b/apps/web/src/modules/FormBuilder/formSchema.ts similarity index 92% rename from apps/web/src/features/FormBuilder/formSchema.ts rename to apps/web/src/modules/FormBuilder/formSchema.ts index e9ea73d3..57d35b51 100644 --- a/apps/web/src/features/FormBuilder/formSchema.ts +++ b/apps/web/src/modules/FormBuilder/formSchema.ts @@ -8,11 +8,11 @@ import { SelectQuestion, ShortAnswerQuestion, UploadQuestion, -} from "@/features/FormBuilder/questions"; -import { FormItemTypes } from "@/features/FormBuilder/types"; +} from "@/modules/FormBuilder/questions"; +import { FormItemTypes } from "@/modules/FormBuilder/types"; import { z } from "zod"; import { nanoid } from "nanoid"; -import { URLQuestion } from "@/features/FormBuilder/questions/url"; +import { URLQuestion } from "@/modules/FormBuilder/questions/url"; export const BaseFormQuestionItemSchema = z.discriminatedUnion("questionType", [ ShortAnswerQuestion.schema, diff --git a/apps/web/src/features/FormBuilder/icons.ts b/apps/web/src/modules/FormBuilder/icons.ts similarity index 100% rename from apps/web/src/features/FormBuilder/icons.ts rename to apps/web/src/modules/FormBuilder/icons.ts diff --git a/apps/web/src/features/FormBuilder/questions/baseQuestion.ts b/apps/web/src/modules/FormBuilder/questions/baseQuestion.ts similarity index 91% rename from apps/web/src/features/FormBuilder/questions/baseQuestion.ts rename to apps/web/src/modules/FormBuilder/questions/baseQuestion.ts index b6e6f68f..600e8b88 100644 --- a/apps/web/src/features/FormBuilder/questions/baseQuestion.ts +++ b/apps/web/src/modules/FormBuilder/questions/baseQuestion.ts @@ -1,4 +1,4 @@ -import { FormItemTypes } from "@/features/FormBuilder/types"; +import { FormItemTypes } from "@/modules/FormBuilder/types"; import z from "zod"; export const BaseQuestion = z.object({ diff --git a/apps/web/src/features/FormBuilder/questions/checkbox.ts b/apps/web/src/modules/FormBuilder/questions/checkbox.ts similarity index 86% rename from apps/web/src/features/FormBuilder/questions/checkbox.ts rename to apps/web/src/modules/FormBuilder/questions/checkbox.ts index 46196d46..e6a65bea 100644 --- a/apps/web/src/features/FormBuilder/questions/checkbox.ts +++ b/apps/web/src/modules/FormBuilder/questions/checkbox.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/createQuestionItem.ts b/apps/web/src/modules/FormBuilder/questions/createQuestionItem.ts similarity index 100% rename from apps/web/src/features/FormBuilder/questions/createQuestionItem.ts rename to apps/web/src/modules/FormBuilder/questions/createQuestionItem.ts diff --git a/apps/web/src/features/FormBuilder/questions/date.ts b/apps/web/src/modules/FormBuilder/questions/date.ts similarity index 84% rename from apps/web/src/features/FormBuilder/questions/date.ts rename to apps/web/src/modules/FormBuilder/questions/date.ts index a35b97c9..c3e5bde4 100644 --- a/apps/web/src/features/FormBuilder/questions/date.ts +++ b/apps/web/src/modules/FormBuilder/questions/date.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/index.ts b/apps/web/src/modules/FormBuilder/questions/index.ts similarity index 100% rename from apps/web/src/features/FormBuilder/questions/index.ts rename to apps/web/src/modules/FormBuilder/questions/index.ts diff --git a/apps/web/src/features/FormBuilder/questions/multipleChoice.ts b/apps/web/src/modules/FormBuilder/questions/multipleChoice.ts similarity index 84% rename from apps/web/src/features/FormBuilder/questions/multipleChoice.ts rename to apps/web/src/modules/FormBuilder/questions/multipleChoice.ts index 149b5b30..0ef4aa99 100644 --- a/apps/web/src/features/FormBuilder/questions/multipleChoice.ts +++ b/apps/web/src/modules/FormBuilder/questions/multipleChoice.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/multiselect.ts b/apps/web/src/modules/FormBuilder/questions/multiselect.ts similarity index 89% rename from apps/web/src/features/FormBuilder/questions/multiselect.ts rename to apps/web/src/modules/FormBuilder/questions/multiselect.ts index 4c5f7e07..fe909bff 100644 --- a/apps/web/src/features/FormBuilder/questions/multiselect.ts +++ b/apps/web/src/modules/FormBuilder/questions/multiselect.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; diff --git a/apps/web/src/features/FormBuilder/questions/number.ts b/apps/web/src/modules/FormBuilder/questions/number.ts similarity index 88% rename from apps/web/src/features/FormBuilder/questions/number.ts rename to apps/web/src/modules/FormBuilder/questions/number.ts index e027519f..ef61a91c 100644 --- a/apps/web/src/features/FormBuilder/questions/number.ts +++ b/apps/web/src/modules/FormBuilder/questions/number.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/paragraph.ts b/apps/web/src/modules/FormBuilder/questions/paragraph.ts similarity index 90% rename from apps/web/src/features/FormBuilder/questions/paragraph.ts rename to apps/web/src/modules/FormBuilder/questions/paragraph.ts index 18f258ad..97cfc8f8 100644 --- a/apps/web/src/features/FormBuilder/questions/paragraph.ts +++ b/apps/web/src/modules/FormBuilder/questions/paragraph.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/select.ts b/apps/web/src/modules/FormBuilder/questions/select.ts similarity index 90% rename from apps/web/src/features/FormBuilder/questions/select.ts rename to apps/web/src/modules/FormBuilder/questions/select.ts index 2981bc63..8b3b3b5a 100644 --- a/apps/web/src/features/FormBuilder/questions/select.ts +++ b/apps/web/src/modules/FormBuilder/questions/select.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/shortAnswer.ts b/apps/web/src/modules/FormBuilder/questions/shortAnswer.ts similarity index 88% rename from apps/web/src/features/FormBuilder/questions/shortAnswer.ts rename to apps/web/src/modules/FormBuilder/questions/shortAnswer.ts index 5de47b83..6e61ec5d 100644 --- a/apps/web/src/features/FormBuilder/questions/shortAnswer.ts +++ b/apps/web/src/modules/FormBuilder/questions/shortAnswer.ts @@ -1,8 +1,8 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; -import { textFieldIcons } from "@/features/FormBuilder/icons"; +import { textFieldIcons } from "@/modules/FormBuilder/icons"; import { errorMessage } from "../errorMessage"; export const ShortAnswerQuestion = createQuestionItem({ diff --git a/apps/web/src/features/FormBuilder/questions/upload.ts b/apps/web/src/modules/FormBuilder/questions/upload.ts similarity index 95% rename from apps/web/src/features/FormBuilder/questions/upload.ts rename to apps/web/src/modules/FormBuilder/questions/upload.ts index cd7d4a20..1307de68 100644 --- a/apps/web/src/features/FormBuilder/questions/upload.ts +++ b/apps/web/src/modules/FormBuilder/questions/upload.ts @@ -1,5 +1,5 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; diff --git a/apps/web/src/features/FormBuilder/questions/url.ts b/apps/web/src/modules/FormBuilder/questions/url.ts similarity index 88% rename from apps/web/src/features/FormBuilder/questions/url.ts rename to apps/web/src/modules/FormBuilder/questions/url.ts index 11a5ff74..1706b80d 100644 --- a/apps/web/src/features/FormBuilder/questions/url.ts +++ b/apps/web/src/modules/FormBuilder/questions/url.ts @@ -1,9 +1,9 @@ -import { createQuestionItem } from "@/features/FormBuilder/questions/createQuestionItem"; -import { QuestionTypes } from "@/features/FormBuilder/types"; +import { createQuestionItem } from "@/modules/FormBuilder/questions/createQuestionItem"; +import { QuestionTypes } from "@/modules/FormBuilder/types"; import z from "zod"; import { BaseQuestion } from "./baseQuestion"; import { errorMessage } from "../errorMessage"; -import { textFieldIcons } from "@/features/FormBuilder/icons"; +import { textFieldIcons } from "@/modules/FormBuilder/icons"; export const URLQuestion = createQuestionItem({ type: QuestionTypes.url, diff --git a/apps/web/src/modules/FormBuilder/scripts/countries.json b/apps/web/src/modules/FormBuilder/scripts/countries.json new file mode 100644 index 00000000..9142114d --- /dev/null +++ b/apps/web/src/modules/FormBuilder/scripts/countries.json @@ -0,0 +1,245 @@ +[ + { "name": "Afghanistan", "code": "AF" }, + { "name": "Åland Islands", "code": "AX" }, + { "name": "Albania", "code": "AL" }, + { "name": "Algeria", "code": "DZ" }, + { "name": "American Samoa", "code": "AS" }, + { "name": "AndorrA", "code": "AD" }, + { "name": "Angola", "code": "AO" }, + { "name": "Anguilla", "code": "AI" }, + { "name": "Antarctica", "code": "AQ" }, + { "name": "Antigua and Barbuda", "code": "AG" }, + { "name": "Argentina", "code": "AR" }, + { "name": "Armenia", "code": "AM" }, + { "name": "Aruba", "code": "AW" }, + { "name": "Australia", "code": "AU" }, + { "name": "Austria", "code": "AT" }, + { "name": "Azerbaijan", "code": "AZ" }, + { "name": "Bahamas", "code": "BS" }, + { "name": "Bahrain", "code": "BH" }, + { "name": "Bangladesh", "code": "BD" }, + { "name": "Barbados", "code": "BB" }, + { "name": "Belarus", "code": "BY" }, + { "name": "Belgium", "code": "BE" }, + { "name": "Belize", "code": "BZ" }, + { "name": "Benin", "code": "BJ" }, + { "name": "Bermuda", "code": "BM" }, + { "name": "Bhutan", "code": "BT" }, + { "name": "Bolivia", "code": "BO" }, + { "name": "Bosnia and Herzegovina", "code": "BA" }, + { "name": "Botswana", "code": "BW" }, + { "name": "Bouvet Island", "code": "BV" }, + { "name": "Brazil", "code": "BR" }, + { "name": "British Indian Ocean Territory", "code": "IO" }, + { "name": "Brunei Darussalam", "code": "BN" }, + { "name": "Bulgaria", "code": "BG" }, + { "name": "Burkina Faso", "code": "BF" }, + { "name": "Burundi", "code": "BI" }, + { "name": "Cambodia", "code": "KH" }, + { "name": "Cameroon", "code": "CM" }, + { "name": "Canada", "code": "CA" }, + { "name": "Cape Verde", "code": "CV" }, + { "name": "Cayman Islands", "code": "KY" }, + { "name": "Central African Republic", "code": "CF" }, + { "name": "Chad", "code": "TD" }, + { "name": "Chile", "code": "CL" }, + { "name": "China", "code": "CN" }, + { "name": "Christmas Island", "code": "CX" }, + { "name": "Cocos (Keeling) Islands", "code": "CC" }, + { "name": "Colombia", "code": "CO" }, + { "name": "Comoros", "code": "KM" }, + { "name": "Congo", "code": "CG" }, + { "name": "Congo, The Democratic Republic of the", "code": "CD" }, + { "name": "Cook Islands", "code": "CK" }, + { "name": "Costa Rica", "code": "CR" }, + { "name": "Cote D\"Ivoire", "code": "CI" }, + { "name": "Croatia", "code": "HR" }, + { "name": "Cuba", "code": "CU" }, + { "name": "Cyprus", "code": "CY" }, + { "name": "Czech Republic", "code": "CZ" }, + { "name": "Denmark", "code": "DK" }, + { "name": "Djibouti", "code": "DJ" }, + { "name": "Dominica", "code": "DM" }, + { "name": "Dominican Republic", "code": "DO" }, + { "name": "Ecuador", "code": "EC" }, + { "name": "Egypt", "code": "EG" }, + { "name": "El Salvador", "code": "SV" }, + { "name": "Equatorial Guinea", "code": "GQ" }, + { "name": "Eritrea", "code": "ER" }, + { "name": "Estonia", "code": "EE" }, + { "name": "Ethiopia", "code": "ET" }, + { "name": "Falkland Islands (Malvinas)", "code": "FK" }, + { "name": "Faroe Islands", "code": "FO" }, + { "name": "Fiji", "code": "FJ" }, + { "name": "Finland", "code": "FI" }, + { "name": "France", "code": "FR" }, + { "name": "French Guiana", "code": "GF" }, + { "name": "French Polynesia", "code": "PF" }, + { "name": "French Southern Territories", "code": "TF" }, + { "name": "Gabon", "code": "GA" }, + { "name": "Gambia", "code": "GM" }, + { "name": "Georgia", "code": "GE" }, + { "name": "Germany", "code": "DE" }, + { "name": "Ghana", "code": "GH" }, + { "name": "Gibraltar", "code": "GI" }, + { "name": "Greece", "code": "GR" }, + { "name": "Greenland", "code": "GL" }, + { "name": "Grenada", "code": "GD" }, + { "name": "Guadeloupe", "code": "GP" }, + { "name": "Guam", "code": "GU" }, + { "name": "Guatemala", "code": "GT" }, + { "name": "Guernsey", "code": "GG" }, + { "name": "Guinea", "code": "GN" }, + { "name": "Guinea-Bissau", "code": "GW" }, + { "name": "Guyana", "code": "GY" }, + { "name": "Haiti", "code": "HT" }, + { "name": "Heard Island and Mcdonald Islands", "code": "HM" }, + { "name": "Holy See (Vatican City State)", "code": "VA" }, + { "name": "Honduras", "code": "HN" }, + { "name": "Hong Kong", "code": "HK" }, + { "name": "Hungary", "code": "HU" }, + { "name": "Iceland", "code": "IS" }, + { "name": "India", "code": "IN" }, + { "name": "Indonesia", "code": "ID" }, + { "name": "Iran, Islamic Republic Of", "code": "IR" }, + { "name": "Iraq", "code": "IQ" }, + { "name": "Ireland", "code": "IE" }, + { "name": "Isle of Man", "code": "IM" }, + { "name": "Israel", "code": "IL" }, + { "name": "Italy", "code": "IT" }, + { "name": "Jamaica", "code": "JM" }, + { "name": "Japan", "code": "JP" }, + { "name": "Jersey", "code": "JE" }, + { "name": "Jordan", "code": "JO" }, + { "name": "Kazakhstan", "code": "KZ" }, + { "name": "Kenya", "code": "KE" }, + { "name": "Kiribati", "code": "KI" }, + { "name": "Korea, Democratic People\"S Republic of", "code": "KP" }, + { "name": "Korea, Republic of", "code": "KR" }, + { "name": "Kuwait", "code": "KW" }, + { "name": "Kyrgyzstan", "code": "KG" }, + { "name": "Lao People\"S Democratic Republic", "code": "LA" }, + { "name": "Latvia", "code": "LV" }, + { "name": "Lebanon", "code": "LB" }, + { "name": "Lesotho", "code": "LS" }, + { "name": "Liberia", "code": "LR" }, + { "name": "Libyan Arab Jamahiriya", "code": "LY" }, + { "name": "Liechtenstein", "code": "LI" }, + { "name": "Lithuania", "code": "LT" }, + { "name": "Luxembourg", "code": "LU" }, + { "name": "Macao", "code": "MO" }, + { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" }, + { "name": "Madagascar", "code": "MG" }, + { "name": "Malawi", "code": "MW" }, + { "name": "Malaysia", "code": "MY" }, + { "name": "Maldives", "code": "MV" }, + { "name": "Mali", "code": "ML" }, + { "name": "Malta", "code": "MT" }, + { "name": "Marshall Islands", "code": "MH" }, + { "name": "Martinique", "code": "MQ" }, + { "name": "Mauritania", "code": "MR" }, + { "name": "Mauritius", "code": "MU" }, + { "name": "Mayotte", "code": "YT" }, + { "name": "Mexico", "code": "MX" }, + { "name": "Micronesia, Federated States of", "code": "FM" }, + { "name": "Moldova, Republic of", "code": "MD" }, + { "name": "Monaco", "code": "MC" }, + { "name": "Mongolia", "code": "MN" }, + { "name": "Montserrat", "code": "MS" }, + { "name": "Morocco", "code": "MA" }, + { "name": "Mozambique", "code": "MZ" }, + { "name": "Myanmar", "code": "MM" }, + { "name": "Namibia", "code": "NA" }, + { "name": "Nauru", "code": "NR" }, + { "name": "Nepal", "code": "NP" }, + { "name": "Netherlands", "code": "NL" }, + { "name": "Netherlands Antilles", "code": "AN" }, + { "name": "New Caledonia", "code": "NC" }, + { "name": "New Zealand", "code": "NZ" }, + { "name": "Nicaragua", "code": "NI" }, + { "name": "Niger", "code": "NE" }, + { "name": "Nigeria", "code": "NG" }, + { "name": "Niue", "code": "NU" }, + { "name": "Norfolk Island", "code": "NF" }, + { "name": "Northern Mariana Islands", "code": "MP" }, + { "name": "Norway", "code": "NO" }, + { "name": "Oman", "code": "OM" }, + { "name": "Pakistan", "code": "PK" }, + { "name": "Palau", "code": "PW" }, + { "name": "Palestinian Territory, Occupied", "code": "PS" }, + { "name": "Panama", "code": "PA" }, + { "name": "Papua New Guinea", "code": "PG" }, + { "name": "Paraguay", "code": "PY" }, + { "name": "Peru", "code": "PE" }, + { "name": "Philippines", "code": "PH" }, + { "name": "Pitcairn", "code": "PN" }, + { "name": "Poland", "code": "PL" }, + { "name": "Portugal", "code": "PT" }, + { "name": "Puerto Rico", "code": "PR" }, + { "name": "Qatar", "code": "QA" }, + { "name": "Reunion", "code": "RE" }, + { "name": "Romania", "code": "RO" }, + { "name": "Russian Federation", "code": "RU" }, + { "name": "RWANDA", "code": "RW" }, + { "name": "Saint Helena", "code": "SH" }, + { "name": "Saint Kitts and Nevis", "code": "KN" }, + { "name": "Saint Lucia", "code": "LC" }, + { "name": "Saint Pierre and Miquelon", "code": "PM" }, + { "name": "Saint Vincent and the Grenadines", "code": "VC" }, + { "name": "Samoa", "code": "WS" }, + { "name": "San Marino", "code": "SM" }, + { "name": "Sao Tome and Principe", "code": "ST" }, + { "name": "Saudi Arabia", "code": "SA" }, + { "name": "Senegal", "code": "SN" }, + { "name": "Serbia and Montenegro", "code": "CS" }, + { "name": "Seychelles", "code": "SC" }, + { "name": "Sierra Leone", "code": "SL" }, + { "name": "Singapore", "code": "SG" }, + { "name": "Slovakia", "code": "SK" }, + { "name": "Slovenia", "code": "SI" }, + { "name": "Solomon Islands", "code": "SB" }, + { "name": "Somalia", "code": "SO" }, + { "name": "South Africa", "code": "ZA" }, + { "name": "South Georgia and the South Sandwich Islands", "code": "GS" }, + { "name": "Spain", "code": "ES" }, + { "name": "Sri Lanka", "code": "LK" }, + { "name": "Sudan", "code": "SD" }, + { "name": "Suriname", "code": "SR" }, + { "name": "Svalbard and Jan Mayen", "code": "SJ" }, + { "name": "Swaziland", "code": "SZ" }, + { "name": "Sweden", "code": "SE" }, + { "name": "Switzerland", "code": "CH" }, + { "name": "Syrian Arab Republic", "code": "SY" }, + { "name": "Taiwan, Province of China", "code": "TW" }, + { "name": "Tajikistan", "code": "TJ" }, + { "name": "Tanzania, United Republic of", "code": "TZ" }, + { "name": "Thailand", "code": "TH" }, + { "name": "Timor-Leste", "code": "TL" }, + { "name": "Togo", "code": "TG" }, + { "name": "Tokelau", "code": "TK" }, + { "name": "Tonga", "code": "TO" }, + { "name": "Trinidad and Tobago", "code": "TT" }, + { "name": "Tunisia", "code": "TN" }, + { "name": "Turkey", "code": "TR" }, + { "name": "Turkmenistan", "code": "TM" }, + { "name": "Turks and Caicos Islands", "code": "TC" }, + { "name": "Tuvalu", "code": "TV" }, + { "name": "Uganda", "code": "UG" }, + { "name": "Ukraine", "code": "UA" }, + { "name": "United Arab Emirates", "code": "AE" }, + { "name": "United Kingdom", "code": "GB" }, + { "name": "United States", "code": "US" }, + { "name": "United States Minor Outlying Islands", "code": "UM" }, + { "name": "Uruguay", "code": "UY" }, + { "name": "Uzbekistan", "code": "UZ" }, + { "name": "Vanuatu", "code": "VU" }, + { "name": "Venezuela", "code": "VE" }, + { "name": "Viet Nam", "code": "VN" }, + { "name": "Virgin Islands, British", "code": "VG" }, + { "name": "Virgin Islands, U.S.", "code": "VI" }, + { "name": "Wallis and Futuna", "code": "WF" }, + { "name": "Western Sahara", "code": "EH" }, + { "name": "Yemen", "code": "YE" }, + { "name": "Zambia", "code": "ZM" }, + { "name": "Zimbabwe", "code": "ZW" } +] diff --git a/apps/web/src/features/FormBuilder/scripts/majors.csv b/apps/web/src/modules/FormBuilder/scripts/majors.csv similarity index 100% rename from apps/web/src/features/FormBuilder/scripts/majors.csv rename to apps/web/src/modules/FormBuilder/scripts/majors.csv diff --git a/apps/web/src/features/FormBuilder/scripts/majors.json b/apps/web/src/modules/FormBuilder/scripts/majors.json similarity index 100% rename from apps/web/src/features/FormBuilder/scripts/majors.json rename to apps/web/src/modules/FormBuilder/scripts/majors.json diff --git a/apps/web/src/features/FormBuilder/scripts/parseCSV.js b/apps/web/src/modules/FormBuilder/scripts/parseCSV.js similarity index 100% rename from apps/web/src/features/FormBuilder/scripts/parseCSV.js rename to apps/web/src/modules/FormBuilder/scripts/parseCSV.js diff --git a/apps/web/src/features/FormBuilder/scripts/schools.csv b/apps/web/src/modules/FormBuilder/scripts/schools.csv similarity index 100% rename from apps/web/src/features/FormBuilder/scripts/schools.csv rename to apps/web/src/modules/FormBuilder/scripts/schools.csv diff --git a/apps/web/src/features/FormBuilder/scripts/schools.json b/apps/web/src/modules/FormBuilder/scripts/schools.json similarity index 100% rename from apps/web/src/features/FormBuilder/scripts/schools.json rename to apps/web/src/modules/FormBuilder/scripts/schools.json diff --git a/apps/web/src/modules/FormBuilder/stories/applicationFormExample.json b/apps/web/src/modules/FormBuilder/stories/applicationFormExample.json new file mode 100644 index 00000000..37845247 --- /dev/null +++ b/apps/web/src/modules/FormBuilder/stories/applicationFormExample.json @@ -0,0 +1,696 @@ +{ + "metadata": { + "title": "SwampHacks XI Application", + "description": "SwampHacks is the University of Florida’s largest annual hackathon. A 36-hour tech event where students from across the country come together to build projects, learn new skills, and connect with fellow innovators. For questions, please email contact@swamphacks.com." + }, + "content": [ + { + "type": "section", + "label": "Personal Information", + "content": [ + { + "type": "layout", + "content": [ + { + "name": "firstName", + "questionType": "shortAnswer", + "placeholder": "Enter your first name", + "label": "First Name", + "required": true, + "validation": { + "maxLength": 50 + } + }, + { + "name": "lastName", + "questionType": "shortAnswer", + "placeholder": "Enter your last name", + "label": "Last Name", + "required": true, + "validation": { + "maxLength": 50 + } + } + ] + }, + { + "type": "layout", + "content": [ + { + "name": "age", + "questionType": "number", + "placeholder": "Enter your age", + "label": "Age", + "required": true, + "validation": { + "min": 0, + "max": 99 + } + }, + { + "name": "phone", + "questionType": "shortAnswer", + "placeholder": "Enter a phone number", + "description": "Ex. 123456789, no dashes and parentheses", + "label": "Phone", + "required": true, + "iconName": "phone", + "validation": { + "maxLength": 10, + "minLength": 10 + } + } + ] + }, + { + "type": "layout", + "content": [ + { + "name": "preferredEmail", + "questionType": "shortAnswer", + "placeholder": "Enter your email", + "label": "Preferred Email", + "required": true, + "iconName": "at", + "validation": { + "email": true + } + }, + { + "name": "universityEmail", + "questionType": "shortAnswer", + "placeholder": "Enter your school email", + "description": "Must be an EDU email. Example: alberta@ufl.edu", + "label": "University Email", + "required": true, + "iconName": "at", + "validation": { + "email": true + }, + "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.edu$" + } + ] + }, + { + "name": "country", + "questionType": "select", + "placeholder": "Select your country", + "label": "Country", + "required": true, + "searchable": true, + "options": { + "data": "countries" + } + }, + { + "type": "layout", + "content": [ + { + "name": "gender", + "questionType": "select", + "placeholder": "Select your gender", + "label": "Gender", + "required": true, + "hasOther": true, + "options": [ + { + "label": "Man", + "value": "man" + }, + { + "label": "Woman", + "value": "woman" + }, + { + "label": "Non-Binary", + "value": "non-binary" + }, + { + "label": "Prefer to self-describe", + "value": "other" + }, + { + "label": "Prefer Not to Answer", + "value": "no-answer" + } + ] + }, + { + "name": "pronouns", + "questionType": "select", + "required": true, + "placeholder": "Select", + "label": "Pronouns", + "options": [ + { + "label": "She/Her", + "value": "she/her" + }, + { + "label": "He/Him", + "value": "he/him" + }, + { + "label": "They/Them", + "value": "they/them" + }, + { + "label": "She/They", + "value": "she/they" + }, + { + "label": "He/They", + "value": "he/they" + }, + { + "label": "Not represented here", + "value": "not-represented" + }, + { + "label": "Prefer Not to Answer", + "value": "no-answer" + } + ] + } + ] + }, + { + "name": "race", + "questionType": "select", + "required": true, + "placeholder": "Select your race", + "label": "Race/Ethnicity", + "hasOther": true, + "options": [ + { + "label": "Native American or Alaska Native", + "value": "native-american-alaska-native" + }, + { + "label": "Asian or Pacific Islander", + "value": "asian-pacific-islander" + }, + { + "label": "Black or African American", + "value": "black-african-american" + }, + { + "label": "Hispanic or Latino", + "value": "hispanic-latino" + }, + { + "label": "White", + "value": "white" + }, + { + "label": "Middle Eastern", + "value": "middle-eastern" + }, + { + "label": "Mixed race", + "value": "multiracial" + }, + { + "label": "Other", + "value": "other" + }, + { + "label": "Prefer Not to Answer", + "value": "no-answer" + } + ] + }, + { + "name": "sexualOrientation", + "questionType": "select", + "required": true, + "placeholder": "Select", + "label": "Do you consider yourself to be any of the following?", + "options": [ + { + "label": "Heterosexual / Straight", + "value": "heterosexual" + }, + { + "label": "Gay / Lesbian", + "value": "homosexual" + }, + { + "label": "Bisexual", + "value": "bisexual" + }, + { + "label": "Other / Not Represented Here", + "value": "not-represented" + }, + { + "label": "Prefer Not to Answer", + "value": "no-answer" + } + ] + }, + { + "name": "linkedin", + "questionType": "url", + "placeholder": "Enter link", + "description": "Must be a full URL. Example: https://www.linkedin.com/in/my-profile", + "label": "Linkedin URL", + "regex": "^(https?:\\/\\/)?(www\\.)?linkedin\\.com\\/in\\/[A-Za-z0-9_-]+\\/?$", + "required": true, + "iconName": "linkedin" + }, + { + "name": "github", + "questionType": "url", + "placeholder": "Enter link", + "description": "Must be a full URL. Example: https://www.github.com/my-profile", + "regex": "^(https?:\\/\\/)?(www\\.)?github\\.com\\/[A-Za-z0-9-]+\\/?$", + "label": "Github URL", + "required": true, + "iconName": "github" + }, + { + "name": "ageCertification", + "questionType": "checkbox", + "required": true, + "options": [ + { + "label": "By clicking this, I certify that I am 18 years old or will turn 18 before the event date.", + "value": "true" + } + ] + } + ] + }, + { + "type": "section", + "label": "Education", + "content": [ + { + "type": "layout", + "content": [ + { + "name": "school", + "questionType": "select", + "placeholder": "Start typing...", + "label": "School", + "required": true, + "searchable": true, + "options": { + "data": "schools" + } + }, + { + "name": "level", + "questionType": "select", + "placeholder": "Select your level", + "label": "Level of Study", + "required": true, + "hasOther": true, + "options": [ + { + "label": "High School", + "value": "high_school" + }, + { + "label": "Undergraduate", + "value": "undergraduate" + }, + { + "label": "Graduate", + "value": "graduate" + }, + { + "label": "Professional / Continuing Education", + "value": "professional" + }, + { + "label": "Other", + "value": "other" + } + ] + } + ] + }, + { + "type": "layout", + "content": [ + { + "name": "year", + "questionType": "select", + "placeholder": "Select your year", + "label": "Year in College", + "required": true, + "hasOther": true, + "options": [ + { + "label": "1st Year", + "value": "first_year" + }, + { + "label": "2nd Year", + "value": "second_year" + }, + { + "label": "3rd Year", + "value": "third_year" + }, + { + "label": "4th Year", + "value": "fourth_year" + }, + { + "label": "Graduate Student", + "value": "graduate" + }, + { + "label": "Other", + "value": "other" + } + ] + }, + { + "name": "graduationYear", + "questionType": "select", + "placeholder": "Select Year", + "label": "Graduation Year", + "required": true, + "options": { + "data": "year", + "min": 2023, + "max": 2050 + } + } + ] + }, + { + "name": "majors", + "questionType": "multiselect", + "placeholder": "Select your major(s)", + "label": "Major(s)", + "required": true, + "options": { + "data": "majors" + } + }, + { + "name": "minors", + "questionType": "shortAnswer", + "placeholder": "Enter your minor(s)", + "label": "Minor(s)", + "required": false + } + ] + }, + { + "type": "section", + "label": "Experience & Preferences", + "content": [ + { + "name": "experience", + "questionType": "multipleChoice", + "label": "How many hackathons have you participated in?", + "required": true, + "options": [ + { + "label": "Swamphacks would be my first!", + "value": "first_time" + }, + { + "label": "1", + "value": "one" + }, + { + "label": "2", + "value": "two" + }, + { + "label": "3", + "value": "three" + }, + { + "label": "4+", + "value": "four_or_more" + } + ] + }, + { + "name": "ufHackathonExp", + "questionType": "multipleChoice", + "label": "Have you attended SwampHacks or any other UF hackathon before?", + "required": true, + "options": [ + { + "label": "Yes, I have.", + "value": "yes" + }, + { + "label": "No, I haven't.", + "value": "no" + } + ] + }, + { + "name": "projectExperience", + "questionType": "multipleChoice", + "label": "Have you created an independent programming project before?", + "required": true, + "options": [ + { + "label": "No, this is my first programming experience", + "value": "no_experience" + }, + { + "label": "No, but I have experience programming in my courses", + "value": "course_experience" + }, + { + "label": "Yes", + "value": "independent_project" + } + ] + }, + { + "name": "shirtSize", + "questionType": "select", + "label": "T-Shirt Size", + "placeholder": "Select your size", + "required": true, + "options": [ + { + "label": "Small", + "value": "S" + }, + { + "label": "Medium", + "value": "M" + }, + { + "label": "Large", + "value": "L" + }, + { + "label": "X-Large", + "value": "XL" + }, + { + "label": "XX-Large", + "value": "XXL" + } + ] + }, + { + "name": "diet", + "questionType": "multiselect", + "placeholder": "Select", + "label": "Dietary Restrictions", + "options": [ + { + "label": "Vegetarian", + "value": "vegetarian" + }, + { + "label": "Vegan", + "value": "vegan" + }, + { + "label": "Celiac Disease", + "value": "celiac-disease" + }, + { + "label": "Allergies", + "value": "allergies" + }, + { + "label": "Kosher", + "value": "kosher" + }, + { + "label": "Halal", + "value": "halal" + }, + { + "label": "Other", + "value": "other" + } + ] + }, + { + "type": "question", + "name": "resume", + "questionType": "upload", + "label": "Upload your resume (PDF files only)", + "description": "By uploading your resume, you consent to us sharing it with our company sponsors.", + "required": true, + "validation": { + "validMimeTypes": "application/pdf" + } + } + ] + }, + { + "type": "section", + "label": "Get To Know You", + "content": [ + { + "name": "essay1", + "questionType": "paragraph", + "placeholder": "Answer here...", + "label": "What is your most memorable experience working in a group? What did you learn and accomplish? (max. 150 words)", + "required": true, + "showWordCount": true, + "validation": { + "max": 150 + } + }, + { + "name": "essay2", + "questionType": "paragraph", + "placeholder": "Answer here...", + "label": "Tell us about a project you are most proud of. (max. 150 words)", + "required": true, + "showWordCount": true, + "validation": { + "max": 150 + } + }, + { + "name": "referral", + "questionType": "checkbox", + "placeholder": "How did you learn about SwampHacks?", + "label": "How did you learn about SwampHacks?", + "required": true, + "options": [ + { + "label": "Our Instagram", + "value": "instagram" + }, + { + "label": "Our Discord", + "value": "discord" + }, + { + "label": "Our LinkedIn", + "value": "linkedin" + }, + { + "label": "Word of Mouth", + "value": "word_of_mouth" + }, + { + "label": "Our Website", + "value": "website" + }, + { + "label": "Class Shoutout", + "value": "class_shoutout" + }, + { + "label": "Other Florida Hackathon", + "value": "other_fl_hackathon" + }, + { + "label": "Other", + "value": "other" + } + ] + } + ] + }, + { + "type": "section", + "label": "SwampHacks Consent & Agreements", + "content": [ + { + "name": "pictureConsent", + "questionType": "checkbox", + "label": "I consent to SwampHacks photo and video policy. We will be taking pictures and recording videos throughout the events for promotional use.", + "required": true, + "options": [ + { + "label": "I agree", + "value": "agree" + } + ] + }, + { + "name": "inpersonAcknowledgement", + "questionType": "checkbox", + "label": "By submitting this application, I recognize that SwampHacks is an in-person only event and my in-person attendance is expected if I am offered acceptance. I also recognize that SwampHacks will be providing free meals but will not be covering transportation costs. ", + "required": true, + "options": [ + { + "label": "I agree", + "value": "agree" + } + ] + } + ] + }, + { + "type": "section", + "label": "MLH Consent & Agreements", + "description": "We are currently in the process of partnering with MLH. The following 3 checkboxes are for this partnership. If we do not end up partnering with MLH, your information will not be shared.", + "content": [ + { + "name": "agreeToConduct", + "questionType": "checkbox", + "label": "I have read and agree to the MLH Code of Conduct.", + "renderLabelAsHTML": true, + "required": true, + "options": [ + { + "label": "I agree", + "value": "agree" + } + ] + }, + { + "name": "infoShareAuthorization", + "questionType": "checkbox", + "label": "I authorize you to share my application/registration information with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH Privacy Policy. I further agree to the terms of both the MLH Contest Terms and Conditions and the MLH Privacy Policy.", + "renderLabelAsHTML": true, + "required": true, + "options": [ + { + "label": "I agree", + "value": "agree" + } + ] + }, + { + "name": "agreeToMLHEmails", + "questionType": "checkbox", + "label": "I authorize MLH to send me occasional emails about relevant events, career opportunities, and community announcements.", + "required": false, + "options": [ + { + "label": "I agree", + "value": "agree" + } + ] + } + ] + } + ] +} diff --git a/apps/web/src/features/FormBuilder/stories/example.json b/apps/web/src/modules/FormBuilder/stories/example.json similarity index 100% rename from apps/web/src/features/FormBuilder/stories/example.json rename to apps/web/src/modules/FormBuilder/stories/example.json diff --git a/apps/web/src/features/FormBuilder/stories/example.stories.tsx b/apps/web/src/modules/FormBuilder/stories/example.stories.tsx similarity index 100% rename from apps/web/src/features/FormBuilder/stories/example.stories.tsx rename to apps/web/src/modules/FormBuilder/stories/example.stories.tsx diff --git a/apps/web/src/features/FormBuilder/test/build.test.ts b/apps/web/src/modules/FormBuilder/test/build.test.ts similarity index 97% rename from apps/web/src/features/FormBuilder/test/build.test.ts rename to apps/web/src/modules/FormBuilder/test/build.test.ts index 59221750..17409e75 100644 --- a/apps/web/src/features/FormBuilder/test/build.test.ts +++ b/apps/web/src/modules/FormBuilder/test/build.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { FormSchema } from "@/features/FormBuilder/formSchema"; +import { FormSchema } from "@/modules/FormBuilder/formSchema"; import validJSON from "./valid.json"; import invalidJSON from "./invalid.json"; import invalidMissingMetadata from "./invalidMissingMetadata.json"; diff --git a/apps/web/src/features/FormBuilder/test/invalid.json b/apps/web/src/modules/FormBuilder/test/invalid.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalid.json rename to apps/web/src/modules/FormBuilder/test/invalid.json diff --git a/apps/web/src/features/FormBuilder/test/invalidMissingMetadata.json b/apps/web/src/modules/FormBuilder/test/invalidMissingMetadata.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalidMissingMetadata.json rename to apps/web/src/modules/FormBuilder/test/invalidMissingMetadata.json diff --git a/apps/web/src/features/FormBuilder/test/invalidMoreThanTwoQuestionsInLayout.json b/apps/web/src/modules/FormBuilder/test/invalidMoreThanTwoQuestionsInLayout.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalidMoreThanTwoQuestionsInLayout.json rename to apps/web/src/modules/FormBuilder/test/invalidMoreThanTwoQuestionsInLayout.json diff --git a/apps/web/src/features/FormBuilder/test/invalidNestedLayouts.json b/apps/web/src/modules/FormBuilder/test/invalidNestedLayouts.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalidNestedLayouts.json rename to apps/web/src/modules/FormBuilder/test/invalidNestedLayouts.json diff --git a/apps/web/src/features/FormBuilder/test/invalidNestedSections.json b/apps/web/src/modules/FormBuilder/test/invalidNestedSections.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalidNestedSections.json rename to apps/web/src/modules/FormBuilder/test/invalidNestedSections.json diff --git a/apps/web/src/features/FormBuilder/test/invalidUnknownFieldType.json b/apps/web/src/modules/FormBuilder/test/invalidUnknownFieldType.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/invalidUnknownFieldType.json rename to apps/web/src/modules/FormBuilder/test/invalidUnknownFieldType.json diff --git a/apps/web/src/features/FormBuilder/test/valid.json b/apps/web/src/modules/FormBuilder/test/valid.json similarity index 100% rename from apps/web/src/features/FormBuilder/test/valid.json rename to apps/web/src/modules/FormBuilder/test/valid.json diff --git a/apps/web/src/features/FormBuilder/types.ts b/apps/web/src/modules/FormBuilder/types.ts similarity index 100% rename from apps/web/src/features/FormBuilder/types.ts rename to apps/web/src/modules/FormBuilder/types.ts diff --git a/apps/web/src/features/NotFound/NotFoundPage.tsx b/apps/web/src/modules/NotFound/NotFoundPage.tsx similarity index 100% rename from apps/web/src/features/NotFound/NotFoundPage.tsx rename to apps/web/src/modules/NotFound/NotFoundPage.tsx diff --git a/apps/web/src/features/Onboarding/components/OnboardingModal.tsx b/apps/web/src/modules/Onboarding/OnboardingModal.tsx similarity index 100% rename from apps/web/src/features/Onboarding/components/OnboardingModal.tsx rename to apps/web/src/modules/Onboarding/OnboardingModal.tsx diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/AddEventModal.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/AddEventModal.tsx similarity index 98% rename from apps/web/src/features/PlatformAdmin/EventManager/components/AddEventModal.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/AddEventModal.tsx index d2785dbc..647609ef 100644 --- a/apps/web/src/features/PlatformAdmin/EventManager/components/AddEventModal.tsx +++ b/apps/web/src/modules/PlatformAdmin/EventManager/AddEventModal.tsx @@ -8,7 +8,7 @@ import { type DateRange, } from "react-aria-components"; import z from "zod"; -import { useCreateAdminEvent } from "../hooks/useCreateAdminEvent"; +import { useCreateAdminEvent } from "./hooks/useCreateAdminEvent"; import { useFormErrors } from "@/components/Form"; import { Modal } from "@/components/ui/Modal"; import { DateRangePicker } from "@/components/ui/DateRangePicker"; diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/AddStaffForm.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/AddStaffForm.tsx similarity index 98% rename from apps/web/src/features/PlatformAdmin/EventManager/components/AddStaffForm.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/AddStaffForm.tsx index 091de7fe..d76e9360 100644 --- a/apps/web/src/features/PlatformAdmin/EventManager/components/AddStaffForm.tsx +++ b/apps/web/src/modules/PlatformAdmin/EventManager/AddStaffForm.tsx @@ -8,7 +8,7 @@ import { useForm } from "@tanstack/react-form"; import { assignStaffRoleSchema, type AssignStaffRole, -} from "../hooks/useAdminStaffActions"; +} from "./hooks/useAdminStaffActions"; export function AddStaffForm({ onSubmit, diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/DeleteEventDialog.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/DeleteEventDialog.tsx similarity index 96% rename from apps/web/src/features/PlatformAdmin/EventManager/components/DeleteEventDialog.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/DeleteEventDialog.tsx index cacfe7e3..70cee5a7 100644 --- a/apps/web/src/features/PlatformAdmin/EventManager/components/DeleteEventDialog.tsx +++ b/apps/web/src/modules/PlatformAdmin/EventManager/DeleteEventDialog.tsx @@ -7,7 +7,7 @@ import { OverlayTriggerStateContext, Text, } from "react-aria-components"; -import { useAdminEventActions } from "../hooks/useAdminEventActions"; +import { useAdminEventActions } from "./hooks/useAdminEventActions"; import { useContext } from "react"; interface DeleteEventDialogProps { diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/EventDetailsCard.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/EventDetailsCard.tsx similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/components/EventDetailsCard.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/EventDetailsCard.tsx diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/ManageEventStaffDialog.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/ManageEventStaffDialog.tsx similarity index 95% rename from apps/web/src/features/PlatformAdmin/EventManager/components/ManageEventStaffDialog.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/ManageEventStaffDialog.tsx index 70cd79e4..cf55a7de 100644 --- a/apps/web/src/features/PlatformAdmin/EventManager/components/ManageEventStaffDialog.tsx +++ b/apps/web/src/modules/PlatformAdmin/EventManager/ManageEventStaffDialog.tsx @@ -1,9 +1,9 @@ import { Heading } from "react-aria-components"; -import { useEventStaffUsers } from "../hooks/useEventStaffUsers"; +import { useEventStaffUsers } from "./hooks/useEventStaffUsers"; import { useAdminStaffActions, type AssignStaffRole, -} from "../hooks/useAdminStaffActions"; +} from "./hooks/useAdminStaffActions"; import { AddStaffForm } from "./AddStaffForm"; import { StaffTable } from "./StaffTable"; import { type ColumnDef } from "@tanstack/react-table"; diff --git a/apps/web/src/features/PlatformAdmin/EventManager/components/StaffTable.tsx b/apps/web/src/modules/PlatformAdmin/EventManager/StaffTable.tsx similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/components/StaffTable.tsx rename to apps/web/src/modules/PlatformAdmin/EventManager/StaffTable.tsx diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminEventActions.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminEventActions.ts similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminEventActions.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminEventActions.ts diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminEvents.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminEvents.ts similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminEvents.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminEvents.ts diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminStaffActions.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminStaffActions.ts similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useAdminStaffActions.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useAdminStaffActions.ts diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts similarity index 95% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts index a76dc44d..bd9e0665 100644 --- a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts +++ b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useCreateAdminEvent.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { type AddEvent } from "../components/AddEventModal"; +import { type AddEvent } from "../AddEventModal"; import { adminEventsQueryKey } from "./useAdminEvents"; import type { CreateEvent, Event } from "@/lib/openapi/types"; import { api } from "@/lib/ky"; diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers.ts similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers.ts diff --git a/apps/web/src/features/PlatformAdmin/EventManager/hooks/useEventUsers.ts b/apps/web/src/modules/PlatformAdmin/EventManager/hooks/useEventUsers.ts similarity index 100% rename from apps/web/src/features/PlatformAdmin/EventManager/hooks/useEventUsers.ts rename to apps/web/src/modules/PlatformAdmin/EventManager/hooks/useEventUsers.ts diff --git a/apps/web/src/features/Redeemables/components/CreateRedeemableModal.tsx b/apps/web/src/modules/Redeemables/CreateRedeemableModal.tsx similarity index 98% rename from apps/web/src/features/Redeemables/components/CreateRedeemableModal.tsx rename to apps/web/src/modules/Redeemables/CreateRedeemableModal.tsx index c5c75bb6..1a0c9dbf 100644 --- a/apps/web/src/features/Redeemables/components/CreateRedeemableModal.tsx +++ b/apps/web/src/modules/Redeemables/CreateRedeemableModal.tsx @@ -6,7 +6,7 @@ import z from "zod"; import { useFormErrors } from "@/components/Form"; import { Modal } from "@/components/ui/Modal"; import { useContext } from "react"; -import { useCreateRedeemable } from "../hooks/useRedeemables"; +import { useCreateRedeemable } from "./hooks/useRedeemables"; import { showToast } from "@/lib/toast/toast"; const createRedeemableSchema = z.object({ diff --git a/apps/web/src/features/Redeemables/components/DeleteRedeemableModal.tsx b/apps/web/src/modules/Redeemables/DeleteRedeemableModal.tsx similarity index 100% rename from apps/web/src/features/Redeemables/components/DeleteRedeemableModal.tsx rename to apps/web/src/modules/Redeemables/DeleteRedeemableModal.tsx diff --git a/apps/web/src/features/Redeemables/components/RedeemableCard.tsx b/apps/web/src/modules/Redeemables/RedeemableCard.tsx similarity index 100% rename from apps/web/src/features/Redeemables/components/RedeemableCard.tsx rename to apps/web/src/modules/Redeemables/RedeemableCard.tsx diff --git a/apps/web/src/features/Redeemables/components/RedeemableDetailsModal.tsx b/apps/web/src/modules/Redeemables/RedeemableDetailsModal.tsx similarity index 99% rename from apps/web/src/features/Redeemables/components/RedeemableDetailsModal.tsx rename to apps/web/src/modules/Redeemables/RedeemableDetailsModal.tsx index cda1b839..9078347d 100644 --- a/apps/web/src/features/Redeemables/components/RedeemableDetailsModal.tsx +++ b/apps/web/src/modules/Redeemables/RedeemableDetailsModal.tsx @@ -12,7 +12,7 @@ import { getUserByRFID, useUpdateRedeemable, useGetCheckedInStatus, -} from "../hooks/useRedeemables"; +} from "./hooks/useRedeemables"; import { DeleteRedeemableModal } from "./DeleteRedeemableModal"; import { Scanner, type IDetectedBarcode } from "@yudiel/react-qr-scanner"; import { showToast } from "@/lib/toast/toast"; diff --git a/apps/web/src/features/Redeemables/hooks/useRedeemables.ts b/apps/web/src/modules/Redeemables/hooks/useRedeemables.ts similarity index 100% rename from apps/web/src/features/Redeemables/hooks/useRedeemables.ts rename to apps/web/src/modules/Redeemables/hooks/useRedeemables.ts diff --git a/apps/web/src/features/Settings/components/SettingsPage.tsx b/apps/web/src/modules/Settings/SettingsPage.tsx similarity index 99% rename from apps/web/src/features/Settings/components/SettingsPage.tsx rename to apps/web/src/modules/Settings/SettingsPage.tsx index 847cdc4e..d0b47afc 100644 --- a/apps/web/src/features/Settings/components/SettingsPage.tsx +++ b/apps/web/src/modules/Settings/SettingsPage.tsx @@ -16,7 +16,7 @@ import TablerLock from "~icons/tabler/lock"; import { settingsFieldsSchema, useSettingsActions, -} from "../hooks/useSettingsActions"; +} from "./hooks/useSettingsActions"; import TablerLogout from "~icons/tabler/logout"; import { useRouter, useCanGoBack } from "@tanstack/react-router"; import TablerHome from "~icons/tabler/home"; diff --git a/apps/web/src/features/Settings/hooks/useSettingsActions.tsx b/apps/web/src/modules/Settings/hooks/useSettingsActions.tsx similarity index 100% rename from apps/web/src/features/Settings/hooks/useSettingsActions.tsx rename to apps/web/src/modules/Settings/hooks/useSettingsActions.tsx diff --git a/apps/web/src/features/Team/components/MyTeamCard.tsx b/apps/web/src/modules/Team/MyTeamCard.tsx similarity index 97% rename from apps/web/src/features/Team/components/MyTeamCard.tsx rename to apps/web/src/modules/Team/MyTeamCard.tsx index 0bdecdd1..5b314aee 100644 --- a/apps/web/src/features/Team/components/MyTeamCard.tsx +++ b/apps/web/src/modules/Team/MyTeamCard.tsx @@ -1,7 +1,7 @@ import { AvatarStack } from "@/components/ui/AvatarStack"; import TablerUsers from "~icons/tabler/users"; -import type { TeamWithMembers } from "../hooks/useMyTeam"; -import { useTeamActions } from "../hooks/useTeamActions"; +import type { TeamWithMembers } from "./hooks/useMyTeam"; +import { useTeamActions } from "./hooks/useTeamActions"; import { Button } from "@/components/ui/Button"; import { toast } from "react-toastify"; import TablerDoorExit from "~icons/tabler/door-exit"; diff --git a/apps/web/src/features/Team/components/NoTeamCard.tsx b/apps/web/src/modules/Team/NoTeamCard.tsx similarity index 94% rename from apps/web/src/features/Team/components/NoTeamCard.tsx rename to apps/web/src/modules/Team/NoTeamCard.tsx index bfdbdb4d..d81f8d13 100644 --- a/apps/web/src/features/Team/components/NoTeamCard.tsx +++ b/apps/web/src/modules/Team/NoTeamCard.tsx @@ -10,7 +10,7 @@ import { newTeamSchema, useTeamActions, type NewTeam, -} from "../hooks/useTeamActions"; +} from "./hooks/useTeamActions"; import { toast } from "react-toastify"; interface Props { @@ -68,7 +68,12 @@ export default function NoTeamCard({ eventId }: Props) {

- + diff --git a/apps/web/src/features/Team/components/TeamCard.tsx b/apps/web/src/modules/Team/TeamCard.tsx similarity index 94% rename from apps/web/src/features/Team/components/TeamCard.tsx rename to apps/web/src/modules/Team/TeamCard.tsx index b384e932..55e0b261 100644 --- a/apps/web/src/features/Team/components/TeamCard.tsx +++ b/apps/web/src/modules/Team/TeamCard.tsx @@ -1,9 +1,9 @@ import { AvatarStack } from "@/components/ui/AvatarStack"; -import type { TeamWithMembers } from "../hooks/useMyTeam"; +import type { TeamWithMembers } from "./hooks/useMyTeam"; import TablerUsers from "~icons/tabler/users"; import TablerCheck from "~icons/tabler/check"; import { Button } from "@/components/ui/Button"; -import { useJoinRequestActions } from "../hooks/useJoinRequestActions"; +import { useJoinRequestActions } from "./hooks/useJoinRequestActions"; import { toast } from "react-toastify"; interface Props { diff --git a/apps/web/src/features/Team/components/TeamInvitationCard.tsx b/apps/web/src/modules/Team/TeamInvitationCard.tsx similarity index 100% rename from apps/web/src/features/Team/components/TeamInvitationCard.tsx rename to apps/web/src/modules/Team/TeamInvitationCard.tsx diff --git a/apps/web/src/features/Team/components/TeamInvitationSection.tsx b/apps/web/src/modules/Team/TeamInvitationSection.tsx similarity index 100% rename from apps/web/src/features/Team/components/TeamInvitationSection.tsx rename to apps/web/src/modules/Team/TeamInvitationSection.tsx diff --git a/apps/web/src/features/Team/components/TeamJoinRequestCard.tsx b/apps/web/src/modules/Team/TeamJoinRequestCard.tsx similarity index 100% rename from apps/web/src/features/Team/components/TeamJoinRequestCard.tsx rename to apps/web/src/modules/Team/TeamJoinRequestCard.tsx diff --git a/apps/web/src/features/Team/components/TeamJoinRequestSection.tsx b/apps/web/src/modules/Team/TeamJoinRequestSection.tsx similarity index 91% rename from apps/web/src/features/Team/components/TeamJoinRequestSection.tsx rename to apps/web/src/modules/Team/TeamJoinRequestSection.tsx index d2fe7875..8c68d67c 100644 --- a/apps/web/src/features/Team/components/TeamJoinRequestSection.tsx +++ b/apps/web/src/modules/Team/TeamJoinRequestSection.tsx @@ -1,8 +1,8 @@ import { Heading } from "react-aria-components"; import TeamJoinRequestCard from "./TeamJoinRequestCard"; import TablerGitPullRequest from "~icons/tabler/git-pull-request"; -import { useTeamPendingJoinRequests } from "../hooks/useTeamPendingJoinRequests"; -import { useJoinRequestActions } from "../hooks/useJoinRequestActions"; +import { useTeamPendingJoinRequests } from "./hooks/useTeamPendingJoinRequests"; +import { useJoinRequestActions } from "./hooks/useJoinRequestActions"; import { toast } from "react-toastify"; interface Props { diff --git a/apps/web/src/features/Team/hooks/useEventTeams.ts b/apps/web/src/modules/Team/hooks/useEventTeams.ts similarity index 85% rename from apps/web/src/features/Team/hooks/useEventTeams.ts rename to apps/web/src/modules/Team/hooks/useEventTeams.ts index 514e7f40..b7fdbebb 100644 --- a/apps/web/src/features/Team/hooks/useEventTeams.ts +++ b/apps/web/src/modules/Team/hooks/useEventTeams.ts @@ -8,7 +8,9 @@ export type TeamsWithMembers = export function useEventTeams(eventId: string, limit: number, offset: number) { async function fetchEventTeams(): Promise { const result = await api - .get(`events/${eventId}/teams?limit=${limit}&offset=${offset}`) + .get( + `events/${eventId}/teams?limit=${limit}&offset=${offset}`, + ) .json(); return result ?? null; } diff --git a/apps/web/src/features/Team/hooks/useJoinRequestActions.ts b/apps/web/src/modules/Team/hooks/useJoinRequestActions.ts similarity index 100% rename from apps/web/src/features/Team/hooks/useJoinRequestActions.ts rename to apps/web/src/modules/Team/hooks/useJoinRequestActions.ts diff --git a/apps/web/src/features/Team/hooks/useMyPendingJoinRequests.ts b/apps/web/src/modules/Team/hooks/useMyPendingJoinRequests.ts similarity index 100% rename from apps/web/src/features/Team/hooks/useMyPendingJoinRequests.ts rename to apps/web/src/modules/Team/hooks/useMyPendingJoinRequests.ts diff --git a/apps/web/src/features/Team/hooks/useMyTeam.ts b/apps/web/src/modules/Team/hooks/useMyTeam.ts similarity index 100% rename from apps/web/src/features/Team/hooks/useMyTeam.ts rename to apps/web/src/modules/Team/hooks/useMyTeam.ts diff --git a/apps/web/src/features/Team/hooks/useTeamActions.ts b/apps/web/src/modules/Team/hooks/useTeamActions.ts similarity index 100% rename from apps/web/src/features/Team/hooks/useTeamActions.ts rename to apps/web/src/modules/Team/hooks/useTeamActions.ts diff --git a/apps/web/src/features/Team/hooks/useTeamPendingJoinRequests.ts b/apps/web/src/modules/Team/hooks/useTeamPendingJoinRequests.ts similarity index 100% rename from apps/web/src/features/Team/hooks/useTeamPendingJoinRequests.ts rename to apps/web/src/modules/Team/hooks/useTeamPendingJoinRequests.ts diff --git a/apps/web/src/features/Users/hooks/useUsers.ts b/apps/web/src/modules/Users/hooks/useUsers.ts similarity index 100% rename from apps/web/src/features/Users/hooks/useUsers.ts rename to apps/web/src/modules/Users/hooks/useUsers.ts diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index ab165707..51867b5f 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,5 +1,5 @@ import { ThemeSwitch } from "@/components/ThemeProvider"; -import NotFoundPage from "@/features/NotFound/NotFoundPage"; +import NotFoundPage from "@/modules/NotFound/NotFoundPage"; import type { auth } from "@/lib/authClient"; import type { QueryClient } from "@tanstack/react-query"; import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; diff --git a/apps/web/src/routes/_protected/_user/portal.tsx b/apps/web/src/routes/_protected/_user/portal.tsx index 136d367e..d8268f16 100644 --- a/apps/web/src/routes/_protected/_user/portal.tsx +++ b/apps/web/src/routes/_protected/_user/portal.tsx @@ -1,9 +1,9 @@ -import { EventCard } from "@/features/Event/components/EventCard"; -import { useEventsWithUserInfo } from "@/features/Event/hooks/useEventsWithUserInfo"; +import { EventCard } from "@/modules/Event/components/EventCard"; +import { useEventsWithUserInfo } from "@/modules/Event/hooks/useEventsWithUserInfo"; import { createFileRoute } from "@tanstack/react-router"; import { Heading, Text } from "react-aria-components"; import { useState } from "react"; -import { OnboardingModal } from "@/features/Onboarding/components/OnboardingModal"; +import { OnboardingModal } from "@/modules/Onboarding/OnboardingModal"; import Cookies from "js-cookie"; import { auth } from "@/lib/authClient"; diff --git a/apps/web/src/routes/_protected/admin/events-management.tsx b/apps/web/src/routes/_protected/admin/events-management.tsx index b9b0e632..dcf4bdef 100644 --- a/apps/web/src/routes/_protected/admin/events-management.tsx +++ b/apps/web/src/routes/_protected/admin/events-management.tsx @@ -2,9 +2,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { Heading, DialogTrigger, Text } from "react-aria-components"; import { Button } from "@/components/ui/Button"; -import { AddEventModal } from "@/features/PlatformAdmin/EventManager/components/AddEventModal"; -import { useAdminEvents } from "@/features/PlatformAdmin/EventManager/hooks/useAdminEvents"; -import { EventDetailsCard } from "@/features/PlatformAdmin/EventManager/components/EventDetailsCard"; +import { AddEventModal } from "@/modules/PlatformAdmin/EventManager/AddEventModal"; +import { useAdminEvents } from "@/modules/PlatformAdmin/EventManager/hooks/useAdminEvents"; +import { EventDetailsCard } from "@/modules/PlatformAdmin/EventManager/EventDetailsCard"; export const Route = createFileRoute("/_protected/admin/events-management")({ component: RouteComponent, diff --git a/apps/web/src/routes/_protected/events/$eventId/application.tsx b/apps/web/src/routes/_protected/events/$eventId/application.tsx index 259049e5..7c6a1bde 100644 --- a/apps/web/src/routes/_protected/events/$eventId/application.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/application.tsx @@ -1,9 +1,9 @@ import { createFileRoute, useRouter } from "@tanstack/react-router"; import { ErrorBoundary } from "react-error-boundary"; -import { ApplicationForm } from "@/features/Application/components/ApplicationForm"; +import { ApplicationForm } from "@/modules/Application/ApplicationForm"; import TablerAlertCircle from "~icons/tabler/alert-circle"; import { useEffect } from "react"; -import { useEvent } from "@/features/Event/hooks/useEvent"; +import { useEvent } from "@/modules/Event/hooks/useEvent"; import { Button } from "@/components/ui/Button"; export const Route = createFileRoute("/_protected/events/$eventId/application")( diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/application-decisions.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/application-decisions.tsx index 2d5ae4eb..a0b17fd1 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/application-decisions.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/application-decisions.tsx @@ -1,4 +1,4 @@ -import { useEvent } from "@/features/Event/hooks/useEvent"; +import { useEvent } from "@/modules/Event/hooks/useEvent"; import { createFileRoute } from "@tanstack/react-router"; import { Heading } from "react-aria-components"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/event-settings.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/event-settings.tsx index 5880d476..8e4d245b 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/event-settings.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/event-settings.tsx @@ -1,6 +1,6 @@ -import EventBannerUploader from "@/features/Event/components/EventBannerUploader"; -import EventSettingsForm from "@/features/Event/components/EventSettingsForm"; -import { useEvent } from "@/features/Event/hooks/useEvent"; +import EventBannerUploader from "@/modules/Event/components/EventBannerUploader"; +import EventSettingsForm from "@/modules/Event/components/EventSettingsForm"; +import { useEvent } from "@/modules/Event/hooks/useEvent"; import { createFileRoute } from "@tanstack/react-router"; import { Heading } from "react-aria-components"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/staff-management.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/staff-management.tsx index 709f2864..cc4127ba 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/staff-management.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/staff-management.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/Button"; -import AddStaffModal from "@/features/EventAdmin/components/AddStaffModal"; -import StaffTable from "@/features/EventAdmin/components/StaffTable"; -import { useEventStaffUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; +import AddStaffModal from "@/modules/EventAdmin/AddStaffModal"; +import StaffTable from "@/modules/EventAdmin/StaffTable"; +import { useEventStaffUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventStaffUsers"; import { createFileRoute } from "@tanstack/react-router"; import { DialogTrigger, Heading } from "react-aria-components"; import { PageLoading } from "@/components/PageLoading"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/user-management.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/user-management.tsx index d415faa6..490c7c96 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/user-management.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_admin/user-management.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useEventUsers } from "@/features/PlatformAdmin/EventManager/hooks/useEventUsers"; -import UserTable from "@/features/EventAdmin/components/UserTable"; +import { useEventUsers } from "@/modules/PlatformAdmin/EventManager/hooks/useEventUsers"; +import UserTable from "@/modules/EventAdmin/UserTable"; import { Heading } from "react-aria-components"; import { PageLoading } from "@/components/PageLoading"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_applicant/application-status.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_applicant/application-status.tsx index 17c96e24..636726c3 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_applicant/application-status.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_applicant/application-status.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import ApplicationStatus from "@/features/Application/components/ApplicationStatus"; +import ApplicationStatus from "@/modules/Application/ApplicationStatus"; export const Route = createFileRoute( "/_protected/events/$eventId/dashboard/_applicant/application-status", diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-review.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-review.tsx index 341991a6..746afd17 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-review.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-review.tsx @@ -1,10 +1,10 @@ import { Button } from "@/components/ui/Button"; import { Tooltip } from "@/components/ui/Tooltip"; -import ResetReviewWarningModal from "@/features/ApplicationReview/components/ResetReviewModal"; -import ApplicationReviewPage from "@/features/ApplicationReview/components/Review/ApplicationReviewPage"; -import ReviewNotStarted from "@/features/ApplicationReview/components/ReviewNotStarted/ReviewNotStarted"; -import { useAppReviewAdminActions } from "@/features/ApplicationReview/hooks/useAppReviewAdminActions"; -import { useEvent } from "@/features/Event/hooks/useEvent"; +import ResetReviewWarningModal from "@/modules/ApplicationReview/ResetReviewModal"; +import ApplicationReviewPage from "@/modules/ApplicationReview/Review/ApplicationReviewPage"; +import ReviewNotStarted from "@/modules/ApplicationReview/ReviewNotStarted/ReviewNotStarted"; +import { useAppReviewAdminActions } from "@/modules/ApplicationReview/hooks/useAppReviewAdminActions"; +import { useEvent } from "@/modules/Event/hooks/useEvent"; import { createFileRoute } from "@tanstack/react-router"; import { DialogTrigger, Heading } from "react-aria-components"; import { toast } from "react-toastify"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-statistics.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-statistics.tsx index 17b8ff35..24e7ede0 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-statistics.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/application-statistics.tsx @@ -1,4 +1,4 @@ -import ApplicationStatistics from "@/features/Application/components/ApplicationStatistics"; +import ApplicationStatistics from "@/modules/Application/ApplicationStatistics"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute( diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/check-in.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/check-in.tsx index 2bb99f29..159a8352 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/check-in.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/check-in.tsx @@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { Heading } from "react-aria-components"; import { Scanner, type IDetectedBarcode } from "@yudiel/react-qr-scanner"; import { useState } from "react"; -import CheckInModal from "@/features/CheckIn/components/CheckInModal"; +import CheckInModal from "@/modules/CheckIn/components/CheckInModal"; import { parseQrIntent } from "@/lib/qr-intents/parse"; import { Intent } from "@/lib/qr-intents/intent"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/redeemables.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/redeemables.tsx index 1070852e..6ae4e43d 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/redeemables.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/_staff/redeemables.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; -import { RedeemableCard } from "@/features/Redeemables/components/RedeemableCard"; -import { CreateRedeemableModal } from "@/features/Redeemables/components/CreateRedeemableModal"; -import { useRedeemables } from "@/features/Redeemables/hooks/useRedeemables"; +import { RedeemableCard } from "@/modules/Redeemables//RedeemableCard"; +import { CreateRedeemableModal } from "@/modules/Redeemables/CreateRedeemableModal"; +import { useRedeemables } from "@/modules/Redeemables/hooks/useRedeemables"; import { Button } from "@/components/ui/Button"; import { DialogTrigger, Heading, Text } from "react-aria-components"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx index 6185bd9e..bfdc21bf 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx @@ -1,5 +1,5 @@ -import AttendeeOverview from "@/features/EventOverview/components/AttendeeOverview"; -import StaffOverview from "@/features/EventOverview/components/StaffOverview"; +import AttendeeOverview from "@/modules/EventOverview/AttendeeOverview"; +import StaffOverview from "@/modules/EventOverview/StaffOverview"; import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/_protected/events/$eventId/dashboard/")({ diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/layout.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/layout.tsx index f32f055c..8670ebe2 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/layout.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/layout.tsx @@ -1,10 +1,10 @@ -import AttendeeAppShell from "@/features/Dashboard/components/AttendeeAppShell"; -import ApplicantAppShell from "@/features/Dashboard/components/ApplicantAppShell"; -import StaffAppShell from "@/features/Dashboard/components/StaffAppShell"; -import { getUserEventRole } from "@/features/Event/api/getUserEventRole"; -import NotFoundPage from "@/features/NotFound/NotFoundPage"; +import AttendeeAppShell from "@/modules/Dashboard/AttendeeAppShell"; +import ApplicantAppShell from "@/modules/Dashboard/ApplicantAppShell"; +import StaffAppShell from "@/modules/Dashboard/StaffAppShell"; +import { getUserEventRole } from "@/modules/Event/api/getUserEventRole"; +import NotFoundPage from "@/modules/NotFound/NotFoundPage"; import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { fetchEvent, getEventQueryKey } from "@/features/Event/hooks/useEvent"; +import { fetchEvent, getEventQueryKey } from "@/modules/Event/hooks/useEvent"; export const Route = createFileRoute("/_protected/events/$eventId/dashboard")({ component: RouteComponent, diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/my-team.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/my-team.tsx index a9cbe670..60d0e6ed 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/my-team.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/my-team.tsx @@ -1,7 +1,7 @@ -import MyTeamCard from "@/features/Team/components/MyTeamCard"; -import NoTeamCard from "@/features/Team/components/NoTeamCard"; -import TeamJoinRequestSection from "@/features/Team/components/TeamJoinRequestSection"; -import { useMyTeam } from "@/features/Team/hooks/useMyTeam"; +import MyTeamCard from "@/modules/Team/MyTeamCard"; +import NoTeamCard from "@/modules/Team/NoTeamCard"; +import TeamJoinRequestSection from "@/modules/Team/TeamJoinRequestSection"; +import { useMyTeam } from "@/modules/Team/hooks/useMyTeam"; import { createFileRoute, notFound } from "@tanstack/react-router"; import { Heading } from "react-aria-components"; diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/teams-explorer.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/teams-explorer.tsx index 807e0225..56a5f735 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/teams-explorer.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/teams-explorer.tsx @@ -1,7 +1,7 @@ -import TeamCard from "@/features/Team/components/TeamCard"; -import { useEventTeams } from "@/features/Team/hooks/useEventTeams"; -import { useMyPendingJoinRequests } from "@/features/Team/hooks/useMyPendingJoinRequests"; -import { useMyTeam } from "@/features/Team/hooks/useMyTeam"; +import TeamCard from "@/modules/Team/TeamCard"; +import { useEventTeams } from "@/modules/Team/hooks/useEventTeams"; +import { useMyPendingJoinRequests } from "@/modules/Team/hooks/useMyPendingJoinRequests"; +import { useMyTeam } from "@/modules/Team/hooks/useMyTeam"; import { createFileRoute, notFound } from "@tanstack/react-router"; import { Heading } from "react-aria-components"; diff --git a/apps/web/src/routes/_protected/settings.tsx b/apps/web/src/routes/_protected/settings.tsx index 1f012004..c60a532b 100644 --- a/apps/web/src/routes/_protected/settings.tsx +++ b/apps/web/src/routes/_protected/settings.tsx @@ -1,4 +1,4 @@ -import { SettingsPage } from "@/features/Settings/components/SettingsPage"; +import { SettingsPage } from "@/modules/Settings/SettingsPage"; import { auth } from "@/lib/authClient"; import { createFileRoute, useRouter } from "@tanstack/react-router"; diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index a6f55bd1..1b747c29 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -import { Login } from "@/features/Auth/components/Login"; +import { Login } from "@/modules/Auth/Login"; import { PageLoading } from "@/components/PageLoading"; import { z } from "zod"; From 91efcf1fce0a293c2ad775fa5fe07621af610b60 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Thu, 2 Apr 2026 18:24:57 -0400 Subject: [PATCH 10/11] quick fix for login --- apps/web/package.json | 2 +- apps/web/src/lib/auth/config.ts | 2 +- apps/web/src/lib/auth/services/user.ts | 2 +- apps/web/src/lib/auth/types/user.ts | 4 +- apps/web/src/lib/openapi/schema.d.ts | 7510 ++++++++++++++---------- apps/web/src/lib/openapi/types.ts | 13 +- apps/web/tsconfig.app.json | 1 - 7 files changed, 4278 insertions(+), 3256 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index e02fb649..f712569f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,7 +14,7 @@ "format": "prettier --write ./src && git add --all", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "generate:openapi": "openapi-typescript ../api/docs/swagger.yaml -o ./src/lib/openapi/schema.d.ts" + "generate-openapi": "openapi-typescript ../api/docs/openapi.json -o ./src/lib/openapi/schema.d.ts" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/apps/web/src/lib/auth/config.ts b/apps/web/src/lib/auth/config.ts index ab29b422..b8dd4e9a 100644 --- a/apps/web/src/lib/auth/config.ts +++ b/apps/web/src/lib/auth/config.ts @@ -3,7 +3,7 @@ import config from "@/config"; export const authConfig = { // General Auth API URLs for SwampHacks Core Backend OAUTH_REDIRECT_URL: `${config.BASE_API_URL}/auth/callback`, - AUTH_ME_URL: `${config.BASE_API_URL}/auth/me`, + AUTH_ME_URL: `${config.BASE_API_URL}/users/me`, AUTH_SESSION_URL: `${config.BASE_API_URL}/auth/session`, AUTH_LOGOUT_URL: `${config.BASE_API_URL}/auth/logout`, diff --git a/apps/web/src/lib/auth/services/user.ts b/apps/web/src/lib/auth/services/user.ts index c3e49493..f296c92e 100644 --- a/apps/web/src/lib/auth/services/user.ts +++ b/apps/web/src/lib/auth/services/user.ts @@ -38,7 +38,7 @@ export async function _getUser(): Promise { // Attempt to parse response in AuthUserResponse schema const userContext = userContextSchema.safeParse(await res.json()); if (!userContext.success) { - console.error("userContext parsing failed"); + console.error("userContext parsing failed: ", userContext.error); return { user: null, error: { diff --git a/apps/web/src/lib/auth/types/user.ts b/apps/web/src/lib/auth/types/user.ts index fa1e9c81..b7b2357f 100644 --- a/apps/web/src/lib/auth/types/user.ts +++ b/apps/web/src/lib/auth/types/user.ts @@ -7,8 +7,10 @@ export const userContextSchema = z.object({ name: z.string(), onboarded: z.boolean(), image: z.string().nullable().optional(), - role: z.enum(["user", "superuser"]), + role: z.enum(['admin', 'staff', 'attendee', 'applicant', 'visitor']), emailConsent: z.boolean(), + checkedInAt: z.date().nullable(), + rfid: z.string().nullable(), }); export type UserContext = z.infer; diff --git a/apps/web/src/lib/openapi/schema.d.ts b/apps/web/src/lib/openapi/schema.d.ts index 8adc198f..b9f7c4fa 100644 --- a/apps/web/src/lib/openapi/schema.d.ts +++ b/apps/web/src/lib/openapi/schema.d.ts @@ -4,3249 +4,4277 @@ */ export interface paths { - "/auth/callback": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * OAuth2 Auth Callback - * @description This route is used for OAuth authentication methods to verify and login/create an account. - */ - post: { - parameters: { - query: { - /** @description The OAuth code passed back from the provider. Part of the PKCE flow. */ - code: string; - /** @description The state containing a base64 encoded version of the nonce, provider, and redirect url. */ - state: string; - }; - header: { - /** @description The nonce for comparing against the callback state decoded to prevent CSRF attacks. */ - sh_auth_nonce: string; - }; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK: User is logged in successfully */ - 200: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Found: Logged in and redirected to a requested location */ - 302: { - headers: { - /** @description Sets a sh_session cookie to signify auth status */ - "Set-Cookie"?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Something went wrong with the request queries or their properties */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Forbidden: Something went wrong verifying identity or authenticating. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Bad Gateway: Authenticating OAuth server did not respond or user does not exist */ - 502: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Current User​ - * @description Get the currently authenticated user's information. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["middleware.UserContext"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Internal Server Error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/email/queue": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Queue an Email Request - * @description Push an email request to the task queue - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Email data */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.QueueTextEmailRequest"]; - }; - }; - responses: { - /** @description OK: Email request queued */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Bad request/Malformed request. The email request is potentially invalid. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: The server went kaput while queueing email sending */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get events - * @description Gets events with a nullable event role for authenticated users. - */ - get: { - parameters: { - query?: { - /** @description Can be scoped to either published, scoped, or all. Scoped means admins and staff can see unpublished events */ - scope?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK: Events returned */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.GetEventsWithUserInfoRow"][]; - }; - }; - }; - }; - put?: never; - /** - * Create a new event - * @description Create a new event with the provided details - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description Event creation data */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.CreateEventFields"]; - }; - }; - responses: { - /** @description OK: Event created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.Event"]; - }; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description endTime is before startTime or applicationClose is before applicationOpen */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get an event - * @description Get a specific event by ID - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Event received */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.Event"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - /** - * Delete an event - * @description Delete an existing event - */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Event deleted */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - options?: never; - head?: never; - /** - * Update an event - * @description Update an existing event - */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Event updated (patched) */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - trace?: never; - }; - "/events/{eventId}/application": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Current User's Application by Event ID - * @description Get the current user's application progress for an event. If this is their first time filling out the application, a new application will be created. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK: An application was found */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | components["schemas"]["sqlc.Application"] - | { - [key: string]: unknown; - }; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error retrieving application"\ */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/{applicationId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get an application based on a user id and event id. - * @description Retrieves an application using the user id and event id primary keys and unique constraints. Only accessible by event staff and admins. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - /** @description Application ID (Technically user ID) */ - applicationId: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: An application was found */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.Application"]; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error retrieving assigned application */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/{applicationId}/resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get resume for application review - * @description This handler creates a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - /** @description The application ID (userId of applicant) */ - applicationId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error handling download resume request */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/assign-reviewers": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign application to reviewers - * @description Assigns applications for an event to reviewers for the application review process. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Reviewer assignmnet payload */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["services.ReviewerAssignment"][]; - }; - }; - responses: { - /** @description Reviewers assigned */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error assigning reviewers */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/assigned": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Assigned Application IDs and Progress - * @description Retrieves assigned applications and their review progress for the authenticated reviewer. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: An application was found */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.AssignedApplication"][]; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error retrieving assigned application */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/download-resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Download the user's uploaded resume from their event application - * @description This handler creates a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error handling download resume request */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/reset-reviews": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reset application reviews - * @description Resets all application reviews for a given event, clearing any existing reviewer assignments. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description ID of the event to reset reviews for */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Application reviews reset successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad request: invalid event ID */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server error: failed to reset application reviews */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/save": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Save Application - * @description Save user's progress on the application. File/Upload fields are not saved. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Form data */ - requestBody: { - content: { - "application/json": Record | Record; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error saving application */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Gets an event's submitted application statistics - * @description This aggregates applications by race, gender, age, majors, and schools. This route is only available to event staff and admins. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.ApplicationStatistics"]; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error getting statistics */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/submit": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit Application - * @description Submit the application for an event. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Submission form data */ - requestBody: { - content: { - "application/json": Record; - "application/x-www-form-urlencoded": Record; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error submitting application */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/application/submit-review": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Submit application review - * @description Handles ratings submissions from staff during the application review process. - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description An object containing the passion and experience ratings */ - requestBody: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error submitting application review */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/interest": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Make an interest submission for an event (email list) - * @description Submit email for event interest/mailing list - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Interest submission data */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.AddEmailRequest"]; - }; - }; - responses: { - /** @description OK: Interest email created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - /** @description Bad request/Malformed request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Duplicate email found in DB */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/overview": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieves general information about the event - * @description Returns data such as event details (name, description, location, dates, etc..) and basic application statistics - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.EventOverview"]; - }; - }; - /** @description Bad request/Malformed request. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: error getting statistics */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/role": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get the current user's event role for an event - * @description Get current user's role for a specific event - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Return role */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["handlers.NullableEventRole"]; - }; - }; - /** @description Not Found - Role not found */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Not Found - Role not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/roles": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Change or add event role of a user - * @description Modify user's role for a specific event - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Event role data */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.AssignRoleFields"]; - }; - }; - responses: { - /** @description OK - Role updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Not Found - User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/roles/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Revoke event role of a user - * @description Remove user's role for a specific event - */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - /** @description User ID */ - userId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK - Role revoked */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Not Found - User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/roles/batch": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Change or add event role of a user in batch - * @description Modify users' role for a specific event - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - /** @description Event roles data */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.AssignRoleBatch"]; - }; - }; - responses: { - /** @description OK - Roles updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": Record; - }; - }; - /** @description Not Found - User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/staff": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all staff users for an event - * @description Gets all users with role STAFF or ADMIN - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.GetEventStaffRow"][]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/teams": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get an event's teams - * @description Gets all teams for a specific event. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the event */ - event_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Teams successfully retrieved. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.TeamWithMembers"][]; - }; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - /** - * Create a new team - * @description Creates a new team for a specific event and assigns the creator as the owner. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the event */ - event_id: number; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - /** @description Team Creation Payload */ - requestBody: { - content: { - "application/json": - | Record - | components["schemas"]["handlers.CreateTeamRequest"]; - }; - }; - responses: { - /** @description A team object */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.Team"]; - }; - }; - /** @description Bad request: you had request parameters needed for this method. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Conflict: You already have a team. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/teams/{teamId}/join": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Request to join a team - * @description Requests to join a team or fails if user is already on a team. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - /** @description The ID of the event */ - event_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - /** @description Team Creation Payload */ - requestBody: { - content: { - "application/json": components["schemas"]["handlers.CreateJoinRequest"]; - }; - }; - responses: { - /** @description Successfully left the team */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Conflict: User is already on a team. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/teams/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get the authenticated user's team and its members for this specific event. - * @description Retrieves the team information and the full list of team members for the currently authenticated user within a specified event. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the event */ - event_id: number; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Team information and members successfully retrieved. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.TeamWithMembers"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Team not found for the user in this event. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/teams/me/pending-joins": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get your pending requests - * @description Retrieves the current user's pending requests for a specific event's teams. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved pending requests */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.TeamJoinRequest"][]; - }; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/events/{eventId}/users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all users for an event - * @description Gets all users with any role for the event - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Event ID */ - eventId: string; - }; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": Record; - }; - }; - responses: { - /** @description OK - Return users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.GetEventStaffRow"][]; - }; - }; - /** @description Server Error: Something went terribly wrong on our end. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a team and its members by team id. - * @description Retrieves the team information and the full list of team members by a team id. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Team information and members successfully retrieved. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["services.TeamWithMembers"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Team not found for the user in this event. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/members/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Kick a member from a team - * @description Kicks a member from a team. Only the team owner can perform this action. - */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - /** @description The ID of the user to be kicked */ - userId: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully kicked the team member */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Forbidden: Requester is not allowed to perform this action. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/members/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Leave a team - * @description Leaves a team if the requester is on the team. Depends on cookies for user retrieval. - */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully left the team */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/{teamId}/pending-joins": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get team's pending join requests - * @description Retrieves a team's pending join requests. This is only allowed for the team's owner. - */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved pending requests */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.ListJoinRequestsByTeamAndStatusWithUserRow"][]; - }; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Forbidden: Requester is not allowed to perform this action. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/join/{requestId}/accept": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Accept a team join request - * @description Accepts a pending team join request. Only the team owner can perform this action. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - /** @description The ID of the join request */ - request_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully accepted the join request */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Forbidden: Requester is not allowed to perform this action. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Not Found: The join request does not exist. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Conflict: The join request has already been responded to. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/teams/join/{requestId}/reject": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reject a team join request - * @description Rejects a pending team join request. Only the team owner can perform this action. - */ - post: { - parameters: { - query?: never; - header?: never; - path: { - /** @description The ID of the team */ - team_id: string; - /** @description The ID of the join request */ - request_id: string; - }; - cookie: { - /** @description The authenticated session token/id */ - sh_session_id: string; - }; - }; - requestBody?: never; - responses: { - /** @description Successfully accepted the join request */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request: Missing or malformed parameters. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Forbidden: Requester is not allowed to perform this action. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Not Found: The join request does not exist. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Conflict: The join request has already been responded to. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get/Search for users - * @description Get or search for users by name or email. If no search term is provided, returns all users with pagination. - */ - get: { - parameters: { - query?: { - /** @description Search term to filter users by name or email (optional) */ - search?: string; - /** @description Maximum number of users to return (default is 50) */ - limit?: number; - /** @description Number of users to skip for pagination (default is 0) */ - offset?: number; - }; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK: Returns a list of users matching the search criteria, or all users if no search term is provided. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.AuthUser"][]; - }; - }; - /** @description Invalid query parameter(s) */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Failed to retrieve users */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/email-consent": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update Email Consent - * @description Update the user's email consent setting - */ - patch: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - /** @description The update email consent request body */ - requestBody: { - content: { - "application/json": components["schemas"]["handlers.UpdateEmailConsentRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid request body */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Failed to update email consent */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - trace?: never; - }; - "/users/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get User Profile - * @description Get profile information of the currently authenticated user. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["sqlc.AuthUser"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description User profile not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Something went seriously wrong. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update User - * @description Update the user's information - */ - patch: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - /** @description The update profile request body */ - requestBody: { - content: { - "application/json": components["schemas"]["handlers.UpdateProfileRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid request body */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Failed to update user profile */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - trace?: never; - }; - "/users/me/onboarding": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Complete Onboarding - * @description Onboard the user. - */ - patch: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie: { - /** @description The authenticated session token/id */ - sh_session: string; - }; - }; - /** @description The onboarding request body */ - requestBody: { - content: { - "application/json": components["schemas"]["handlers.CompleteOnboardingRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid request body */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Unauthenticated: Requester is not currently authenticated. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - /** @description Failed to complete onboarding */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["response.ErrorResponse"]; - }; - }; - }; - }; - trace?: never; - }; + "/application": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Application + * @description Get the application of the current user + */ + get: operations["get-application"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/accept-acceptance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Accept Application Acceptance + * @description Accept an acceptance after being accepted. Sets event role to attendee, from applicant. + */ + patch: operations["accept-application-acceptance"]; + trace?: never; + }; + "/application/assigned": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Assigned Applications + * @description Returns assigned applications and their review progress for the authenticated reviewer + */ + get: operations["get-assigned-applications"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/calculate-admissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit Admissions Calculation Request + * @description Queues an admission calculation task to the BAT worker + */ + post: operations["calculate-admissions-request"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/join-waitlist": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Join Waitlist + * @description Adds a waitlist join time to application. Sets status to waitlisted + */ + patch: operations["join-waitlist"]; + trace?: never; + }; + "/application/release-decisions/{runId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Release Decisions + * @description Releases decisions that were calculated by the worker from a specific run id + */ + post: operations["release-decisions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/resume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Resume Download URL + * @description Returns a presigned S3 URL with GET permission for the user's specific object, which is their uploaded resume. The client can use this URL to download the object. + */ + get: operations["get-download-resume-url"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/review/assign": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Assign Application Reviewers + * @description Assigns applications to reviewers for the application review process. + */ + post: operations["assign-application-reviewers"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/review/reset": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reset Application Reviews + * @description Resets all application reviews, clearing any existing reviewer assignments. + */ + post: operations["reset-application-reviews"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/review/{applicantId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit Application Review + * @description Handles ratings submissions from staff during the application review process + */ + post: operations["submit-application-review"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/review/{applicantId}/resume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Resume URL (for review process) + * @description Returns a presigned S3 URL with GET permission for a specific user's resume as an object. The client can use this URL to download the object temporarily for application review. + */ + get: operations["get-resume"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/save": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Save Application + * @description Save user's progress on the application. File/Upload fields are not saved (eg. resumes). + */ + post: operations["save-application"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Application Statistics + * @description Aggregates applications by race, gender, age, majors, and schools + */ + get: operations["get-application-statistics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/submit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit Application + * @description Submit the application + */ + post: operations["submit-application"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/application/transition-waitlisted-applications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Transition Waitlisted Applications + * @description Transitions all accepted users to waitlist, and accepts 50 from the waitlist. Sets application status from accepted to rejected. + */ + patch: operations["transition-waitlist"]; + trace?: never; + }; + "/application/withdraw-acceptance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Withdraw Acceptance + * @description Withdraw an acceptance after being accepted to an event. Sets application status from accepted to rejected. + */ + patch: operations["withdraw-acceptance"]; + trace?: never; + }; + "/application/withdraw-attendance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Withdraw Attendance + * @description Withdraw attendance after accepting to go to the hackathon. Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant. + */ + patch: operations["withdraw-attendance"]; + trace?: never; + }; + "/auth/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * OAuth Callback + * @description Handles the OAuth provider callback, validates state and nonce, and sets the session cookie. + */ + get: operations["oauth-callback"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Logout + * @description Logs out the authenticated user by invalidating their session + */ + post: operations["logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Hackathon + * @description Returns information of the hackathon + */ + get: operations["get-hackathon"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update Hackathon + * @description Updates the information of the hackathon + */ + patch: operations["update-hackathon"]; + trace?: never; + }; + "/hackathon/attendees/count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Hackathon Attendees Count + * @description Returns the number of users who is attending the hackathon + */ + get: operations["get-hackathon-attendees-count"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon/attendees/discord": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Hackathon Attendees with Discord + * @description Returns all users with a discord account that is also attending the hackathon + */ + get: operations["get-hackathon-attendees-with-discord"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon/attendees/userids": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Hackathon Attendees User Ids + * @description Returns all users ids of users who are attending the hackathon + */ + get: operations["get-hackathon-attendees-userids"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon/checkin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Check In User + * @description Staff route for checking a user to an event. The user to check in must be an attendee and have never been checked in yet. + */ + get: operations["check-in"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon/interest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit Interest Email + * @description Submits an email to interest/mailing list for the hackathon + */ + post: operations["submit-interest-email"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/hackathon/staff": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Hackathon Staff + * @description Returns the users who are part of the current staff of the hackathon + */ + get: operations["get-hackathon-staff"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/redeemables": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Redeemables + * @description Returns a list of all redeemable items + */ + get: operations["get-redeemables"]; + put?: never; + /** + * Create Redeemable + * @description Creates a new redeemable item + */ + post: operations["create-redeemable"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/redeemables/{redeemableId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Redeemable + * @description Deletes a redeemable by id + */ + delete: operations["delete-redeemable"]; + options?: never; + head?: never; + /** + * Update Redeemable + * @description Update specific fields (name, stock, max per user) of a redeemable + */ + patch: operations["update-redeemable"]; + trace?: never; + }; + "/redeemables/{redeemableId}/users/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Redeem Redeemable + * @description Redeems a redeemable by id. Creates a redemption record linking a specific user to a redeemable item + */ + post: operations["redeem-redeemable"]; + delete?: never; + options?: never; + head?: never; + /** + * Update Redemption + * @description Updates a redemption created by the user. + */ + patch: operations["update-redemption"]; + trace?: never; + }; + "/teams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Team + * @description Creates a new team and assigns the user as the owner. Returns the team. + */ + post: operations["create-team"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get My Team + * @description Returns the team information and the full list of team members for the currently authenticated user + */ + get: operations["get-my-team"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/me/pending-joins": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get User's Pending Join Requests + * @description Returns the current user's pending requests for teams. + */ + get: operations["get-my-pending-join-requests"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{requestId}/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept Team Join Request + * @description Accepts a pending team join request. Only the team owner can perform this action. + */ + post: operations["accept-team-join-request"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{requestId}/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject Team Join Request + * @description Rejects a pending team join request. Only the team owner can perform this action. + */ + post: operations["reject-team-join-request"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Team + * @description Returns the team information and the full list of team members by team id + */ + get: operations["get-team"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/join": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Request to Join Team + * @description Requests to join a team or fails if user is already on a team. + */ + post: operations["create-join-team-request"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/kick/{memberId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Kick Team Member + * @description Kicks a member from a team. Only the team owner can perform this action. + */ + post: operations["kick-member-from-team"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/leave": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Leave Team + * @description Leaves a team if the user is on the team. + */ + post: operations["leave-team"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/teams/{teamId}/pending-joins": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Pending Join Requests for Team + * @description Returns a team's pending join requests. This is only allowed for the team's owner. + */ + get: operations["get-pending-join-team-requests"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Users + * @description Get or search for users by name or email. If no search term is provided, returns all users with pagination. + */ + get: operations["get-users"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/email/{email}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get User By Email + * @description Returns the user associated with the email + */ + get: operations["get-user-by-email"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Me + * @description Returns the authenticated user's profile + */ + get: operations["get-me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update User + * @description Updates information of the authenticated user + */ + patch: operations["update-user"]; + trace?: never; + }; + "/users/me/email-consent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update Email Consent + * @description Updates the user's email consent setting + */ + patch: operations["update-email-consent"]; + trace?: never; + }; + "/users/me/onboarding": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Onboard User + * @description Allows the user to submit information such as name and preferred email, and complete the onboarding process + */ + patch: operations["onboard-user"]; + trace?: never; + }; + "/users/rfid/{rfid}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get User By RFID + * @description Returns the user associated with the RFID + */ + get: operations["get-user-by-rfid"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/roles/assign": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Assign Role + * @description Assigns/modify a user's role + */ + post: operations["assign-role"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/roles/batch-assign": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Batch Assign Roles + * @description Batch assign/modify multiple users' roles + */ + post: operations["batch-assign-roles"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/roles/revoke/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Revoke Role + * @description Remove a user's role + */ + post: operations["revoke-role"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/userid/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get User By Id + * @description Returns the user associated with the user id + */ + get: operations["get-user-by-id"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - "handlers.AddEmailRequest": { - email: string; - source: string; - }; - "handlers.AssignRoleBatch": { - assignments: components["schemas"]["handlers.AssignRoleFields"][]; - }; - "handlers.AssignRoleFields": { - email: string; - role: components["schemas"]["sqlc.EventRoleType"]; - user_id: string; - }; - "handlers.CompleteOnboardingRequest": { - name: string; - preferred_email: string; - }; - "handlers.CreateEventFields": { - application_close: string; - application_open: string; - decision_release: string; - description: string; - end_time: string; - is_published: boolean; - location: string; - location_url: string; - max_attendees: number; - name: string; - rsvp_deadline: string; - start_time: string; - website_url: string; - }; - "handlers.CreateJoinRequest": { - message: string; - }; - "handlers.CreateTeamRequest": { - name: string; - }; - "handlers.NullableEventRole": { - assigned_at: string; - event_id: string; - role: components["schemas"]["sqlc.EventRoleType"]; - user_id: string; - }; - "handlers.QueueTextEmailRequest": { - body: string; - subject: string; - to: string[]; - }; - "handlers.UpdateEmailConsentRequest": { - email_consent: boolean; - }; - "handlers.UpdateProfileRequest": { - name: string; - preferred_email: string; - }; - /** @description Information about the current user session. */ - "middleware.UserContext": { - /** - * @description Primary email address (nullable) - * @example user@example.com - */ - email: string; - /** - * @description Whether the user agreed to receive emails - * @example false - */ - emailConsent: boolean; - /** - * @description Optional profile image URL - * @example https://cdn.example.com/avatar.png - */ - image: string | null; - /** - * @description Full display name - * @example Jane Doe - */ - name: string; - /** - * @description Whether the user completed onboarding - * @example true - */ - onboarded: boolean; - /** - * @description Preferred email address for communications - * @example user.alt@example.com - */ - preferredEmail: string; - role: components["schemas"]["sqlc.AuthUserRole"]; - /** - * Format: uuid - * @description Unique identifier for the user - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - userId: string; - }; - "response.ErrorResponse": { - error: string; - message: string; - }; - /** @enum {string} */ - "services.ApplicationReviewStatus": "in_progress" | "completed"; - "services.ApplicationStatistics": { - age_stats: components["schemas"]["sqlc.GetApplicationAgeSplitRow"]; - gender_stats: components["schemas"]["sqlc.GetApplicationGenderSplitRow"]; - major_stats: components["schemas"]["sqlc.GetApplicationMajorSplitRow"][]; - race_stats: components["schemas"]["sqlc.GetApplicationRaceSplitRow"][]; - school_stats: components["schemas"]["sqlc.GetApplicationSchoolSplitRow"][]; - status_stats: components["schemas"]["sqlc.GetApplicationStatusSplitRow"]; - }; - "services.AssignedApplication": { - status: components["schemas"]["services.ApplicationReviewStatus"]; - user_id: string; - }; - "services.EventOverview": { - application_status_stats: components["schemas"]["sqlc.GetApplicationStatusSplitRow"]; - application_submission_stats: components["schemas"]["services.SubmissionTimesStatistics"][]; - event_details: components["schemas"]["sqlc.Event"]; - }; - "services.MemberWithUserInfo": { - email: string; - image: string; - joined_at: string; - name: string; - user_id: string; - }; - "services.ReviewerAssignment": { - /** @description Number of applications assigned (nil if autoassign) */ - amount: number; - /** @description User/Reviewer ID */ - id: string; - }; - "services.SubmissionTimesStatistics": { - count: number; - /** Format: date-time */ - day: string; - }; - "services.TeamWithMembers": { - event_id: string; - id: string; - members: components["schemas"]["services.MemberWithUserInfo"][]; - name: string; - owner_id: string; - }; - "sqlc.Application": { - application: number[]; - assigned_reviewer_id: string; - created_at: string; - event_id: string; - experience_rating: number; - passion_rating: number; - saved_at: string; - status: components["schemas"]["sqlc.NullApplicationStatus"]; - submitted_at: string; - updated_at: string; - user_id: string; - }; - /** @enum {string} */ - "sqlc.ApplicationStatus": - | "started" - | "submitted" - | "under_review" - | "accepted" - | "rejected" - | "waitlisted" - | "withdrawn"; - "sqlc.AuthUser": { - created_at: string; - email: string; - email_consent: boolean; - email_verified: boolean; - id: string; - image: string; - name: string; - onboarded: boolean; - preferred_email: string; - role: components["schemas"]["sqlc.AuthUserRole"]; - updated_at: string; - }; - /** - * @description Role assigned to the user - * @enum {string} - */ - "sqlc.AuthUserRole": "user" | "superuser"; - "sqlc.Event": { - application_close: string; - application_open: string; - application_review_started: boolean; - banner: string; - created_at: string; - decision_release: string; - description: string; - end_time: string; - id: string; - is_published: boolean; - location: string; - location_url: string; - max_attendees: number; - name: string; - rsvp_deadline: string; - start_time: string; - updated_at: string; - website_url: string; - }; - /** @enum {string} */ - "sqlc.EventRoleType": "admin" | "staff" | "attendee" | "applicant"; - "sqlc.GetApplicationAgeSplitRow": { - age_18: number; - age_19: number; - age_20: number; - age_21: number; - age_22: number; - age_23_plus: number; - underage: number; - }; - "sqlc.GetApplicationGenderSplitRow": { - female: number; - male: number; - non_binary: number; - other: number; - }; - "sqlc.GetApplicationMajorSplitRow": { - count: number; - major: string; - }; - "sqlc.GetApplicationRaceSplitRow": { - count: number; - race_group: string; - }; - "sqlc.GetApplicationSchoolSplitRow": { - count: number; - school: string; - }; - "sqlc.GetApplicationStatusSplitRow": { - accepted: number; - rejected: number; - started: number; - submitted: number; - under_review: number; - waitlisted: number; - withdrawn: number; - }; - "sqlc.GetEventStaffRow": { - created_at: string; - email: string; - email_consent: boolean; - email_verified: boolean; - event_role: components["schemas"]["sqlc.EventRoleType"]; - id: string; - image: string; - name: string; - onboarded: boolean; - preferred_email: string; - role: components["schemas"]["sqlc.AuthUserRole"]; - updated_at: string; - }; - "sqlc.GetEventsWithUserInfoRow": { - application_close: string; - application_open: string; - application_review_started: boolean; - application_status: components["schemas"]["sqlc.NullApplicationStatus"]; - banner: string; - created_at: string; - decision_release: string; - description: string; - end_time: string; - event_role: components["schemas"]["sqlc.NullEventRoleType"]; - id: string; - is_published: boolean; - location: string; - location_url: string; - max_attendees: number; - name: string; - rsvp_deadline: string; - start_time: string; - updated_at: string; - website_url: string; - }; - /** @enum {string} */ - "sqlc.JoinRequestStatus": "PENDING" | "APPROVED" | "REJECTED"; - "sqlc.ListJoinRequestsByTeamAndStatusWithUserRow": { - created_at: string; - id: string; - processed_at: string; - processed_by_user_id: string; - request_message: string; - status: components["schemas"]["sqlc.JoinRequestStatus"]; - team_id: string; - updated_at: string; - user_email: string; - user_id: string; - user_image: string; - user_name: string; - }; - "sqlc.NullApplicationStatus": { - application_status: components["schemas"]["sqlc.ApplicationStatus"]; - /** @description Valid is true if ApplicationStatus is not NULL */ - valid: boolean; - }; - "sqlc.NullEventRoleType": { - event_role_type: components["schemas"]["sqlc.EventRoleType"]; - /** @description Valid is true if EventRoleType is not NULL */ - valid: boolean; - }; - "sqlc.Team": { - created_at: string; - event_id: string; - id: string; - name: string; - owner_id: string; - updated_at: string; - }; - "sqlc.TeamJoinRequest": { - created_at: string; - id: string; - processed_at: string; - processed_by_user_id: string; - request_message: string; - status: components["schemas"]["sqlc.JoinRequestStatus"]; - team_id: string; - updated_at: string; - user_id: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + Application: { + application: string; + assigned_reviewer_id: string; + /** Format: date-time */ + created_at: string; + /** Format: int32 */ + experience_rating: number | null; + hackathon_iteration: string; + /** Format: int32 */ + passion_rating: number | null; + /** Format: date-time */ + saved_at: string; + status: components["schemas"]["NullApplicationStatus"]; + /** Format: date-time */ + submitted_at: string | null; + /** Format: date-time */ + updated_at: string; + user_id: string; + /** Format: date-time */ + waitlist_join_time: string | null; + }; + ApplicationStatistics: { + age_stats: components["schemas"]["GetApplicationAgeSplitRow"]; + gender_stats: components["schemas"]["GetApplicationGenderSplitRow"]; + major_stats: components["schemas"]["GetApplicationMajorSplitRow"][] | null; + race_stats: components["schemas"]["GetApplicationRaceSplitRow"][] | null; + school_stats: components["schemas"]["GetApplicationSchoolSplitRow"][] | null; + status_stats: components["schemas"]["GetApplicationStatusSplitRow"]; + }; + AssignRoleBatchRequest: { + assignments: components["schemas"]["AssignRoleRequest"][] | null; + }; + AssignRoleRequest: { + email: string | null; + role: string; + user_id: string | null; + }; + AssignedApplication: { + applicantId: string; + status: string; + }; + CheckInRequest: { + rfid: string | null; + user_id: string; + }; + CreateJoinRequest: { + message: string | null; + }; + CreateRedeemableRequest: { + /** Format: int64 */ + amount: number; + /** Format: int64 */ + max_user_amount: number; + name: string; + }; + CreateTeamRequest: { + name: string; + }; + ErrorDetail: { + /** @description Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' */ + location?: string; + /** @description Error message text */ + message?: string; + /** @description The value at the given location */ + value?: unknown; + }; + ErrorModel: { + /** @description A human-readable explanation specific to this occurrence of the problem. */ + detail?: string; + /** @description Optional list of individual error details */ + errors?: components["schemas"]["ErrorDetail"][] | null; + /** + * Format: uri + * @description A URI reference that identifies the specific occurrence of the problem. + */ + instance?: string; + /** + * Format: int64 + * @description HTTP status code + */ + status?: number; + /** @description A short, human-readable summary of the problem type. This value should not change between occurrences of the error. */ + title?: string; + /** + * Format: uri + * @description A URI reference to human-readable documentation for the error. + * @default about:blank + */ + type: string; + }; + GetApplicationAgeSplitRow: { + /** Format: int64 */ + age_18: number; + /** Format: int64 */ + age_19: number; + /** Format: int64 */ + age_20: number; + /** Format: int64 */ + age_21: number; + /** Format: int64 */ + age_22: number; + /** Format: int64 */ + age_23_plus: number; + /** Format: int64 */ + underage: number; + }; + GetApplicationGenderSplitRow: { + /** Format: int64 */ + female: number; + /** Format: int64 */ + male: number; + /** Format: int64 */ + non_binary: number; + /** Format: int64 */ + other: number; + }; + GetApplicationMajorSplitRow: { + /** Format: int64 */ + count: number; + major: string; + }; + GetApplicationRaceSplitRow: { + /** Format: int64 */ + count: number; + race_group: string; + }; + GetApplicationSchoolSplitRow: { + /** Format: int64 */ + count: number; + school: string; + }; + GetApplicationStatusSplitRow: { + /** Format: int64 */ + accepted: number; + /** Format: int64 */ + rejected: number; + /** Format: int64 */ + started: number; + /** Format: int64 */ + submitted: number; + /** Format: int64 */ + under_review: number; + /** Format: int64 */ + waitlisted: number; + /** Format: int64 */ + withdrawn: number; + }; + GetAttendeesWithDiscordRow: { + discord_id: string; + email: string | null; + name: string; + user_id: string; + }; + GetRedeemablesRow: { + /** Format: date-time */ + created_at: string | null; + id: string; + /** Format: int32 */ + max_user_amount: number; + name: string; + total_redeemed: unknown; + /** Format: int32 */ + total_stock: number; + /** Format: date-time */ + updated_at: string | null; + }; + Hackathon: { + /** Format: date-time */ + application_close: string; + /** Format: date-time */ + application_open: string; + application_review_started: boolean; + banner: string | null; + /** Format: date-time */ + created_at: string | null; + /** Format: date-time */ + decision_release: string | null; + description: string | null; + /** Format: date-time */ + end_time: string; + is_published: boolean | null; + location: string | null; + location_url: string | null; + /** Format: int32 */ + max_attendees: number | null; + name: string; + onerow_id: boolean; + /** Format: date-time */ + rsvp_deadline: string | null; + /** Format: date-time */ + start_time: string; + /** Format: date-time */ + updated_at: string | null; + website_url: string | null; + }; + ListJoinRequestsByTeamAndStatusWithUserRow: { + /** Format: date-time */ + created_at: string; + id: string; + /** Format: date-time */ + processed_at: string | null; + processed_by_user_id: string; + request_message: string | null; + status: string; + team_id: string; + /** Format: date-time */ + updated_at: string; + user_email: string | null; + user_id: string; + user_image: string | null; + user_name: string; + }; + MemberWithUserInfo: { + email: string | null; + image: string | null; + /** Format: date-time */ + joined_at: string | null; + name: string; + user_id: string; + }; + NullApplicationStatus: { + application_status: string; + valid: boolean; + }; + OnboardingRequest: { + name: string; + preferred_email: string; + }; + Redeemable: { + /** Format: int32 */ + amount: number; + /** Format: date-time */ + created_at: string | null; + id: string; + /** Format: int32 */ + max_user_amount: number; + name: string; + /** Format: date-time */ + updated_at: string | null; + }; + ReviewRatings: { + /** Format: int64 */ + experience_rating: number; + /** Format: int64 */ + passion_rating: number; + }; + ReviewerAssignment: { + /** Format: int64 */ + amount: number | null; + userId: string; + }; + SubmitInterestEmailRequest: { + email: string; + source: string | null; + }; + Team: { + /** Format: date-time */ + created_at: string | null; + id: string; + name: string; + owner_id: string; + /** Format: date-time */ + updated_at: string | null; + }; + TeamJoinRequest: { + /** Format: date-time */ + created_at: string; + id: string; + /** Format: date-time */ + processed_at: string | null; + processed_by_user_id: string; + request_message: string | null; + status: string; + team_id: string; + /** Format: date-time */ + updated_at: string; + user_id: string; + }; + TeamWithMembers: { + id: string; + members: components["schemas"]["MemberWithUserInfo"][] | null; + name: string; + owner_id: string; + }; + UpdateEmailConsentRequest: { + email_consent: boolean; + }; + UpdateHackathonRequest: { + /** Format: date-time */ + application_close: string; + /** Format: date-time */ + application_open: string; + /** Format: date-time */ + decision_release: string | null; + description: string | null; + /** Format: date-time */ + end_time: string; + is_published: boolean; + location: string | null; + location_url: string | null; + /** Format: int32 */ + max_attendees: number | null; + name: string; + /** Format: date-time */ + rsvp_deadline: string | null; + /** Format: date-time */ + start_time: string; + website_url: string | null; + }; + UpdateRedeemableRequest: { + /** Format: int64 */ + max_user_amount?: number; + name?: string; + /** Format: int64 */ + total_stock?: number; + }; + UpdateRedemptionRequest: { + /** Format: int64 */ + new_amount?: number; + }; + UpdateUserRequest: { + name: string; + preferred_email: string; + }; + User: { + /** Format: date-time */ + checked_in_at: string | null; + /** Format: date-time */ + created_at: string; + email: string | null; + email_consent: boolean; + email_verified: boolean; + id: string; + image: string | null; + name: string; + onboarded: boolean; + preferred_email: string | null; + rfid: string | null; + role: string; + /** Format: date-time */ + role_assigned_at: string | null; + /** Format: date-time */ + updated_at: string; + }; + UserContext: { + /** Format: date-time */ + checkedInAt: string | null; + email: string | null; + emailConsent: boolean; + image: string | null; + name: string; + onboarded: boolean; + preferredEmail: string | null; + rfid: string | null; + /** @enum {string} */ + role: "admin" | "staff" | "attendee" | "applicant" | "visitor"; + /** Format: uuid */ + userId: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; -export type operations = Record; +export interface operations { + "get-application": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Application"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "accept-application-acceptance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-assigned-applications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssignedApplication"][] | null; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "calculate-admissions-request": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "join-waitlist": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "release-decisions": { + parameters: { + query?: never; + header?: never; + path: { + runId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-download-resume-url": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "assign-application-reviewers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReviewerAssignment"][] | null; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "reset-application-reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "submit-application-review": { + parameters: { + query?: never; + header?: never; + path: { + applicantId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReviewRatings"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-resume": { + parameters: { + query?: never; + header?: never; + path: { + applicantId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "save-application": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: { + content: { + "application/json": unknown; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-application-statistics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApplicationStatistics"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "submit-application": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "transition-waitlist": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "withdraw-acceptance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "withdraw-attendance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "oauth-callback": { + parameters: { + query: { + /** @description OAuth authorization code */ + code: string; + /** @description Base64 encoded OAuth state */ + state: string; + }; + header?: { + /** @description Client user agent */ + "User-Agent"?: string; + }; + path?: never; + cookie: { + /** @description Auth nonce cookie for CSRF protection */ + sh_auth_nonce: string; + }; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + Location?: string; + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Implemented */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-hackathon": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Hackathon"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "update-hackathon": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateHackathonRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-hackathon-attendees-count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-hackathon-attendees-with-discord": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAttendeesWithDiscordRow"][] | null; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-hackathon-attendees-userids": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string[] | null; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "check-in": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CheckInRequest"]; + }; + }; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "submit-interest-email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SubmitInterestEmailRequest"]; + }; + }; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-hackathon-staff": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"][] | null; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-redeemables": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetRedeemablesRow"][] | null; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "create-redeemable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRedeemableRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Redeemable"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "delete-redeemable": { + parameters: { + query?: never; + header?: never; + path: { + redeemableId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "update-redeemable": { + parameters: { + query?: never; + header?: never; + path: { + redeemableId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRedeemableRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "redeem-redeemable": { + parameters: { + query?: never; + header?: never; + path: { + redeemableId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "update-redemption": { + parameters: { + query?: never; + header?: never; + path: { + redeemableId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRedemptionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "create-team": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTeamRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Team"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-my-team": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamWithMembers"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-my-pending-join-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamJoinRequest"][] | null; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "accept-team-join-request": { + parameters: { + query?: never; + header?: never; + path: { + requestId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "reject-team-join-request": { + parameters: { + query?: never; + header?: never; + path: { + requestId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-team": { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamWithMembers"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "create-join-team-request": { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateJoinRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamJoinRequest"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "kick-member-from-team": { + parameters: { + query?: never; + header?: never; + path: { + memberId: string; + teamId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "leave-team": { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-pending-join-team-requests": { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListJoinRequestsByTeamAndStatusWithUserRow"][] | null; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-users": { + parameters: { + query?: { + search?: string; + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"][] | null; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-user-by-email": { + parameters: { + query?: never; + header?: never; + path: { + email: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserContext"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "update-user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateUserRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "update-email-consent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateEmailConsentRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "onboard-user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OnboardingRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-user-by-rfid": { + parameters: { + query?: never; + header?: never; + path: { + rfid: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "assign-role": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AssignRoleRequest"]; + }; + }; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "batch-assign-roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AssignRoleBatchRequest"]; + }; + }; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "revoke-role": { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; + "get-user-by-id": { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie: { + /** @description Session cookie used to authenticate the user */ + sh_session_id: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; +} diff --git a/apps/web/src/lib/openapi/types.ts b/apps/web/src/lib/openapi/types.ts index 23117e61..9d01cace 100644 --- a/apps/web/src/lib/openapi/types.ts +++ b/apps/web/src/lib/openapi/types.ts @@ -1,12 +1,5 @@ import type { components, paths } from "./schema"; -export type ErrorResponse = components["schemas"]["response.ErrorResponse"]; -export type UserContext = components["schemas"]["middleware.UserContext"]; -export type PlatformRole = components["schemas"]["sqlc.AuthUserRole"]; -export type Event = components["schemas"]["sqlc.Event"]; -export type CreateEvent = - paths["/events"]["post"]["requestBody"]["content"]["application/json"]; -export type User = components["schemas"]["sqlc.AuthUser"]; - -export type EventWithUserInfo = - components["schemas"]["sqlc.GetEventsWithUserInfoRow"]; +// export type ErrorResponse = components["schemas"]["response.ErrorResponse"]; +export type UserContext = components["schemas"]["UserContext"]; +// export type Role = components["schemas"][""]; diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index 1ee294f5..3e569a70 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -24,7 +24,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "baseUrl": ".", "paths": { "@/*": ["./src/*"] } From 8e7a9878dc69de315d76c36ddfbfe0ef307b66ed Mon Sep 17 00:00:00 2001 From: Hieu Nguyen <76720778+hieunguyent12@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:35:20 -0400 Subject: [PATCH 11/11] Docs/hackathon management (#323) * docs (hackathon mangagement): add development tips * docs (hackathon management): fix wording, fix formatting, make file name more descript * fix (hackathon mangagement): wording * fix (hackathon mangagement): wording --------- Co-authored-by: h1divp <71522316+h1divp@users.noreply.github.com> --- apps/docs/mkdocs.yml | 8 +- apps/docs/src/letter-to-new-people.md | 128 ++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 apps/docs/src/letter-to-new-people.md diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 69b5976a..cbc6d602 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -49,7 +49,7 @@ nav: - System Architecture: architecture.md - Repository Structure: repo-structure.md - Getting Started: getting-started.md - - Development Workflow: workflow.md + - Letter To New Tech Teams: letter-to-new-people.md - Web: - Overview: web/index.md - Installation & Setup: web/installation.md @@ -84,4 +84,8 @@ nav: - Docs: - Overview: docs/index.md - Installation: docs/installation.md - - Writing Guide: docs/writing-guide.md + - Hackathon Management: + - Overview: hackathon-management/index.md + +extra: + generator: false diff --git a/apps/docs/src/letter-to-new-people.md b/apps/docs/src/letter-to-new-people.md new file mode 100644 index 00000000..2b12ede0 --- /dev/null +++ b/apps/docs/src/letter-to-new-people.md @@ -0,0 +1,128 @@ +Hi there, Phoenix here. + +One thing that seems to be not taught in school and mostly picked up on the fly is good software engineering practices. I don't even think SWE teaches these. + +As part of the SH tech team, you are entrusted with the valuable task of making sure everything goes well. The frameworks you choose and the way you solve problems ultimately will not matter besides getting the expected functionality that everyone wants and making your deadlines. This means that, despite there +usually being more than one way to solve most of your problems, you're probably going to want to choose ones that will save you and your team members the most headache. There will also be people who may read your code long after you leave and go on to do greater things. + +On top of this, some weeks you're a busy student without much time to get things done, and, well, may have to implement something the quick and dirty way and refactor later. +This is called "technical debt" and as a member of this team, please try to understand it very well. + +I certainly think that being a part of this team will give you a lot of practical software engineering knowledge and the ability to have a positive impact on many people at a scale which you will have probably not had before: on the organizers, your team members, and ultimately, hundreds if not hopefully well over a thousand users. This document is to specifically give you advice and things to research about that will make you be able to work towards this end as best as possible and achieve amazing results. Following it may prevent a lot of unnecessary headaches, bad blood, and late nights. + +### Everyone *will* make mistakes +In other words, nothing that you're going to write will be bug-less. Unexpected behavior will happen even after testing, and you have to ensure that things won't go +wrong when they do. For example, with sending emails in the email system, we made sure that both successes and failures were logged, and that they included the attempted contact email. What do you think would happen if we didn't do this? After a batch of decisions emails were sent, there potentially could have been hundreds of failures that would go unnoticed and lead to worse hackathon turnout. And if we had to re-do an email send, duplicate emails could have been sent if we didn't know who already probably received one. If we had to re-run the decisions algorithm too, then there would then be the potential for someone to receive both an acceptance and rejection email. Some simple future-proofing can avoid bad situations like these, which may I remind you can still happen even on well tested code. + +### Atomicity +Noting the above example with decisions emails, what if we made the whole thing get kicked off with a single request to one API endpoint, deciding acceptances and *then* sending emails immediately after? + +Well, what happens if all of the results created by the algorithm accidentally only accepted seniors? Congratulations, you would have sent a bunch of incorrect decisions emails and the organizers are now drafting an apology announcement (or maybe they put that on you). That's a big no no! + +Preventing this situation would be done by breaking apart the functionality for the decisions feature into separately runnable pieces of code. Or in other words, making the decisions functionality more "atomic". + +We did this by letting us calculate the decisions *first* using one endpoint, and after inspecting the data and confirming with the organizers that the statistics looked the way we wanted, we would hit another endpoint to queue up all of the emails which would then be sent out. Remember this when making features especially for public facing functionality. + +### Resiliency +This is a more dev-ops related problem, but a fun one. + +When working on SH you're going to be interacting with a lot of very valuable, mission critical data. But maybe someone was accidentally were testing something on the production database and deleted all of the precious user data. Maybe AWS is down (this actually happened on decision day lol). What now? + +Well, you're probably thinking about backups. Great! But if your backups are online somewhere and the internet pipes stop working, then, hmm... what to do... +And are you only backing up production data? If so, how often should you do it? You're going to need a strategy. + +My suggestion is to use a 3-2-1 backup strategy. If you find a better strategy, and everyone agrees with it, then great. use that. But at least strategize *how* you're going to do the backups. And the earlier you set that up the better. Basically: + +* You want *3* copies of your data + +* On *2* types of storage media (SSDs and HDDs can fail differently, use a tape drive if you want peak reliability) + +* and most importantly *1* backup in a different location from your main db (offsite). + +For our database we used Neon, which is just a managed Postgres db you can interface with. At the time of writing Neon allows you to rollback your database, but that couldn't be done past a certain elapsed time and was expensive. Thus, I wrote a script to dump the database to a personal server that I owned every 6 hours during the event, as well as before huge changes were made, like running the decisions algo. You will probably want something more substantial than that, and I hope this team manages to create a more permanent backup solution. Realistically, while people are signing up, a backup should be made at least once a day. + +### Erroring, logging, and observability +Expect errors. Expect them in places where you think they would never happen, even though the program works with validated inputs or whatever. This is also one huge benefit for using Golang for our backend. Many functions return an error value and un-used variables force the program to not compile. That's an intentional choice by the authors of this language skewing developers to better programming practices. + +Catching errors everywhere in your code can really save a lot of wasted time chasing odd behavior or bugs. You may also find that developing one feature months after another will cause some errors to throw that you've previously written. You'll be very glad you wrote them if that happens! + +We also used something called a structured logging tool. This basically means that logs output to the console have a certain type and fields. + +This may seem like it just makes prettier console output, but if these are taken in by an observability tool (like Grafana) you can actually send out a notification to the tech team when a spike of errors hits the system, or when certain errors happen that you *know* shouldn't. Say for example, you could set an alert for a warning log saying that someone's UserID can't be found while running functionality on a page only authorized people have access to, potentially shedding light on a security vulnerability. That's big. + +### Clever solution != a smart one +In other words, the very complicated cool way of doing things should be avoided over a simple and easy to understand solution. + +You should aim to write software that looks so plain and simple that everything it does is obvious. Reduce complexity at all costs. + +You might understand some complicated functionality very well. You may have put blood, sweat, and tears into it. But after you leave this team, +someone else may have to try to understand that, and if they can't, it might be easier for them to rewrite it. To prevent mass time loss across +you and other people, you should strive to make all functionality as clear and simple as possible. There are many guides online explaining how to do this. + +Some hints I follow while programming that tells me when my code might be complicated are usually: + +* my code begins nesting too much + +* the order which functionality happens is not clear + +* the functionality of a certain block of code is not clear + +* what package is that function in again...? + +I would also avoid + +* tracking too much state (avoid state desyncronization) + +And try to + +* group functionality together as well as possible, file and folder wise + * yes i have problems with the way the current backend is structured. look at a more mature go repository for the reason why. + +### Your code should be self documenting +Comments are important, but they should not be over-utilized. Code should be simple enough to the point where what happens is clearly understandable, like I've been saying. + +However, sometimes explaining what something does is best done with a comment. But when you do, understand that you are making an assumption that the person reading your code won't understand things too clearly. How much do you think they know? + +I.e., you need to understand your audience when writing them. + +In my opinion, people maintaining this codebase should have at least a working proficiency with the libraries and tech stack used, or are otherwise expected to learn enough to be at that level. Especially +whoever is reading the segment of code you're commenting on. When writing your comments, you should assume your readers know basics so you don't need to over-explain. + +One practical example of this could be with HTTP status codes. People working on the HTTP API should be expected to know what these are, so you don't need to explain what 400 and 500 codes are and what kinds of errors they should be returned in. + +However, this is to a point. When dealing with HTTP status codes, you could also assume that even those familiar with them may not have seen +certain ones in a while. Let's say you're returning a request on an error, and the user is unauthorized. We might just set a status code +field to the number 401, but to some people skimming through that http handler there is a good chance they may only have a vauge idea of what a 401 status is, and have to look it up on MDN web docs if they decide that's worth their time (key point: they might not). Instead, a better decision would be to name a variable called `StatusUnauthorized`, +set it to the value `401`, and then put that in the field. + +Thankfully, Golang's net/http library has this for every status code. You should use them, like we did :) + +### Communicating with organizers (or managers, or anyone else you have to report to) +I put this at the bottom because I probably don't want them to see this lol. + +Please realize that while yes, the organizers may be quite pestering in wanting many things to be done, they have a lot of pressure +and unfortunately cannot just take your place and achieve everything they want. They have to instead entrust *you* and the entire tech +team things that they are responsible for. And the degree of that is super high, like making sure that everyone who shows up on the +day of the hackathon were the people who were actually accepted. Or sending out hundreds of notification emails. They certainly cannot do that by themselves. + +Assuming the last people selected good organizers, they will probably be really ambitious and ask a lot from the tech team. That can be overwhelming. + +Also, some of the things they ask for may also be unrealistic or incoherent with schedules, the state of the current codebase, etc. +While that might be intimidating at first, please understand that as a member of this team you still have a lot of agency with what gets +done and are completely free to argue about cutting features, suggesting an alternative solution to something they expect, etc. + +It is important that when they really want something done, to keep them in the loop and respond timely. But you are also able to set their expectations when you say what things are currently like. Please understand that you can use that to make the everyone on the team's life easier or more difficult. Not just yours. + +### Blame +If something goes terribly wrong, I urge you to not try and single out any one person as the reason why something happened the way it happened. + +Instead, let it be a learning opportunity for everyone. This can mean making a priority for testing, or peer reviewing code, spending more money to properly back up data and the like. Large mistakes in my opinion are usually caused by a large chain of events, and more safe guards or more attention to something can usually prevent mistakes in the future. But good organizational decisions have to be made. + +Singling someone out is also going to de-moralize one out of the five or so developers you have and probably make them stressed as hell or not want to work, which can also put more responsibility on everyone else. Do you really want that? Please think calmly before releasing your frustration. + +## Ending notes + +I know this is a long read. But you also have a lot of work ahead of you. I hope you find some of the points here at least interesting or something worth some thought. +Again, it may save you and a lot of other people hassle they're probably never thinking about. + +Good Luck Have Fun :)