diff --git a/backend/api.json b/backend/api.json index 8be962fe..9cb1f9d1 100644 --- a/backend/api.json +++ b/backend/api.json @@ -1,1220 +1,1022 @@ { - "openapi": "3.0.0", - "info": { - "title": "Chaos API", - "version": "1.0.0" + "openapi": "3.0.0", + "info": { + "title": "Chaos API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://chaos.csesoc.app/api", + "description": "Production server" }, - "servers": [ - { - "url": "https://chaos.csesoc.app/api", - "description": "Production server" - }, - { - "url": "http://localhost:3000/api", - "description": "Local server" - } - ], - "paths": { - "/auth/logout": { - "post": { - "operationId": "logout", - "description": "Invalidates current token.", - "tags": [ - "Auth" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "messages": { - "type": "string", - "example": "Successfully logged out." - } - } - } + { + "url": "http://localhost:3000/api", + "description": "Local server" + } + ], + "paths": { + "/": { + "get": { + "operationId": "getRoot", + "description": "Root of API", + "tags": ["Miscellaneous"], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "Join DevSoc! https://devsoc.app/" } } } } } - }, - "/user": { - "get": { - "operationId": "getLoggedInUser", - "description": "Returns info about currently logged in user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "email": { - "type": "string", - "example": "me@example.com" - }, - "zid": { - "type": "string", - "example": "z5555555" - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "degree_name": { - "type": "string", - "example": "Computer Science" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 - }, - "role": { - "type": "string", - "example": "User" - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } - } - } + } + }, + "/auth/callback/google": { + "get": { + "operationId": "googleCallback", + "description": "Google OAuth callback", + "tags": ["Auth"], + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Google OAuth code", + "required": true, + "schema": { + "type": "string" } } - }, - "delete": { - "operationId": "deleteUserById", - "description": "Deletes currently logged in user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted user." - } + ], + "responses": { + "200": { + "description": "Ok", + "content": null + } + } + } + }, + "/auth/logout": { + "post": { + "operationId": "logout", + "description": "Invalidates current token", + "tags": ["Auth"], + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully logged out" } } } } - }, - "401": { - "description": "Not logged.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotLoggedIn" } } - }, - "403": { - "description": "User is only admin of an organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Cannot delete sole admin of an organisation." - } - } - } + } + } + } + } + }, + "/user": { + "get": { + "operationId": "getLoggedInUser", + "description": "Returns info about currently logged in user", + "tags": ["User"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/user/{id}": { - "get": { - "operationId": "getUserById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "User ID", - "required": true, + } + }, + "/user/name": { + "patch": { + "operationId": "updateUserName", + "description": "Updates currently logged in user's name", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified user.", - "tags": [ - "User" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "email": { - "type": "string", - "example": "me@example.com" - }, - "zid": { - "type": "string", - "example": "z5555555" - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "degree_name": { - "type": "string", - "example": "Computer Science" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 - } - } - } - } - } - }, - "403": { - "description": "Requested user has not applied to one of authorized user's campaign.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Insufficient permissions" - } - } + "properties": { + "name": { + "type": "string", + "example": "Clancy Tiger" } } } } } - } - }, - "/user/name": { - "patch": { - "operationId": "updateUserName", - "description": "Updates currently logged in user's name.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { + "message": { "type": "string", - "example": "Clancy Tiger" + "example": "Successfully updated name" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated name." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/pronouns": { + "patch": { + "operationId": "updateUserPronouns", + "description": "Updates currently logged in user's pronouns", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "pronouns": { + "type": "string", + "example": "They/Them" } } } } } - } - }, - "/user/zid": { - "patch": { - "operationId": "updateUserZid", - "description": "Updates currently logged in user's zID.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "zid": { + "message": { "type": "string", - "example": "z5123456" + "example": "Successfully updated pronouns" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated zID." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/gender": { + "patch": { + "operationId": "updateUserGender", + "description": "Updates currently logged in user's gender", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "gender": { + "type": "string", + "example": "Female" } } } } } - } - }, - "/user/degree": { - "patch": { - "operationId": "updateUserDegree", - "description": "Updates currently logged in user's degree.", - "tags": [ - "User" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "degree_name": { + "message": { "type": "string", - "example": "Electrical Engineering" - }, - "degree_starting_year": { - "type": "integer", - "example": 2024 + "example": "Successfully updated gender" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated email." - } - } - } - } - } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/user/zid": { + "patch": { + "operationId": "updateUserZid", + "description": "Updates currently logged in user's zID", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "zid": { + "type": "string", + "example": "z5123456" } } } } } - } - }, - "/organisation": { - "post": { - "operationId": "createOrganisation", - "description": "Creates a new organisation.", - "tags": [ - "Organisation" - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { + "message": { "type": "string", - "example": "UNSW Software Development Society" - }, - "admin": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500, - "description": "User ID of admin" + "example": "Successfully updated zID" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully created organisation." - } - } - } - } - } - }, - "403": { - "description": "User is not a super user.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } - } - } - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}": { - "get": { - "operationId": "getOrganisationById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/user/degree": { + "patch": { + "operationId": "updateUserDegree", + "description": "Updates currently logged in user's degree", + "tags": ["User"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "logo": { - "type": "string", - "example": "76718252-2a13-4de2-bc07-f977c75dc52b" - }, - "created_at": { - "type": "string", - "example": "2024-02-10T18:25:43.511Z" - } - } + "properties": { + "degree_name": { + "type": "string", + "example": "Electrical Engineering" + }, + "degree_starting_year": { + "type": "integer", + "example": 2024 } } } } } }, - "delete": { - "operationId": "deleteOrganisationById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Deletes specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted organisation." - } - } - } - } - } - }, - "403": { - "description": "User is not a super user.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated email" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}/campaigns": { - "get": { - "operationId": "getOrganisationCampaignsById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns active campaigns for specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "campaigns": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } - } - } + } + }, + "/user/applications": { + "get": { + "operationId": "getUserApplications", + "description": "Returns info about applications made by currently logged in user", + "tags": ["User"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationDetails" } } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/organisation/{id}/logo": { - "patch": { - "operationId": "updateOrganisationLogoById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/organisation": { + "post": { + "operationId": "createOrganisation", + "description": "Create a new organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Updates logo for specified organistion.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "upload_url": { - "type": "string", - "description": "Presigned S3 url to upload file.", - "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJJWZ7B6WCRGMKFGQ%2F20180210%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20180210T171315Z&X-Amz-Expires=1800&X-Amz-Signature=12b74b0788aa036bc7c3d03b3f20c61f1f91cc9ad8873e3314255dc479a25351&X-Amz-SignedHeaders=host" - } - } + "properties": { + "slug": { + "type": "string", + "description": "ASCII string for URL like https://chaos.csesoc.app/s/unsw-devsoc", + "example": "unsw-devsoc" + }, + "name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "admin": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500, + "description": "User ID of admin" } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created organisation" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "403": { + "description": "User is not a SuperUser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unauthorized" } } } } } - }, - "/organisation/{id}/members": { - "get": { - "operationId": "getOrganisationMembersById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, + } + }, + "/organisation/slug_check": { + "post": { + "operationId": "checkOrganisationSlugAvailability", + "description": "Check if slug is available", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns list of members of specified organisation.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "members": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036500 - }, - "name": { - "type": "string", - "example": "Clancy Lion" - }, - "role": { - "type": "string", - "example": "Admin" - } - } - } - } - } - } - } - } - }, - "403": { - "description": "User is not an organisation admin or member.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } + "properties": { + "slug": { + "type": "string", + "example": "unsw-devsoc" } } } } } }, - "put": { - "operationId": "updateOrganisationMembersById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "members": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "integer", - "format": "int64" - }, - "example": [ - 1541815603606036500, - 1541815603606036700, - 1541815287306036500 - ] - } - } - } - } - } - }, - "description": "Specifies members for specified organistion.", - "tags": [ - "Organisation" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated members." - } + "message": { + "type": "string", + "example": "Organisation slug is available" } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } - } - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "400": { + "description": "Bad request - slug is in use or not ASCII", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequest" } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } - } + } + } + } + } + }, + "/organisation/{id}": { + "get": { + "operationId": "getOrganisationById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationDetails" } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } }, - "/organisation/{id}/campaign": { - "post": { - "operationId": "createCampaign", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Organisation ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Creates a new campaign inside specified organisation.", - "tags": [ - "Organisation" - ], - "requestBody": { + "delete": { + "operationId": "deleteOrganisationById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { + "message": { "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" + "example": "Successfully deleted organisation" } } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully created campaign." - } + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "403": { + "description": "User is not a SuperUser", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Unauthorized" + } + } + } + } + } + } + }, + "/organisation/slug/{slug}": { + "get": { + "operationId": "getOrganisationBySlug", + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "Organisation slug", + "required": true, + "schema": { + "type": "string" + } + } + ], + "description": "Returns info about specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationDetails" + } + } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + } + } + } + }, + "/organisation/{id}/campaign": { + "post": { + "operationId": "createCampaign", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Create a new campaign inside specified organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewCampaign" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created campaign" } } } } - }, - "403": { - "description": "User is not an admin of specified organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/campaign/slug_check": { + "post": { + "operationId": "checkCampaignSlugAvailability", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Checks availability of campaign slug in specified organisation", + "tags": ["Organisation"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Campaign slug is available" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" } } - }, - "/campaign": { - "get": { - "operationId": "getAllCampaigns", - "description": "Returns all active campaigns.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "campaigns": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "organisation_id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036700 - }, - "organisation_name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } - } - } + } + }, + "/organisation/{id}/campaigns": { + "get": { + "operationId": "getAllOrganisationCampaigns", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns all (active & ended) campaigns for specified organisation. However, ended campaigns cannot have new applications", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganisationCampaign" } } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" } } - }, - "/campaign/{id}": { - "get": { - "operationId": "getCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, + } + }, + "/organisation/{id}/email_template": { + "post": { + "operationId": "createEmailTemplate", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Create a new email template within the organisation", + "tags": ["Organisation"], + "requestBody": { + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Returns info about specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "format": "int64", - "example": 6996987893965263000 - }, - "organisation_id": { - "type": "integer", - "format": "int64", - "example": 1541815603606036700 - }, - "organisation_name": { - "type": "string", - "example": "UNSW Software Development Society" - }, - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "cover_image": { - "type": "string", - "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { - "type": "string", - "example": "2024-04-15T18:25:43.511Z" - } + "$ref": "#/components/schemas/NewEmailTemplate" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created email template" } } } } } + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" } - }, - "put": { - "operationId": "updateCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" + } + } + }, + "/organisation/{id}/email_templates": { + "get": { + "operationId": "getAllOrganisationEmailTemplates", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Get all email templates for specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmailTemplate" + } + } } } - ], - "requestBody": { + }, + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/logo": { + "patch": { + "operationId": "updateOrganisationLogoById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Update logo for specified organistion. Returns a PUT url to upload new image to", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "properties": { - "name": { - "type": "string", - "example": "2024 Subcommittee Recruitment" - }, - "description": { - "type": "string", - "example": "Are you excited to make a difference?" - }, - "starts_at": { - "type": "string", - "example": "2024-03-15T18:25:43.511Z" - }, - "ends_at": { + "upload_url": { "type": "string", - "example": "2024-04-15T18:25:43.511Z" + "description": "Presigned S3 url to upload file", + "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d" } } } } } }, - "description": "Updates details of specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully updated campaign." + "307": { + "$ref": "#/components/responses/NotLoggedIn" + }, + "401": { + "$ref": "#/components/responses/NotOrganisationAdmin" + } + } + } + }, + "/organisation/{id}/member": { + "get": { + "operationId": "getOrganisationMembersById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns list of members of specified organisation", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "role": { + "type": "string", + "example": "Admin" + } + } } } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + }, + "403": { + "description": "User is not an organisation admin or member.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } - } + } + } + } + }, + "put": { + "operationId": "updateOrganisationMembersById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "format": "int64" + }, + "example": [ + 1541815603606036500, 1541815603606036700, + 1541815287306036500 + ] } } } } } }, - "delete": { - "operationId": "deleteCampaignById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Deletes specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Successfully deleted campaign." - } + "description": "Specifies members for specified organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." } } } } - }, - "403": { - "description": "User is not an admin of campaign's organisation.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" - } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } @@ -1223,74 +1025,1908 @@ } } }, - "/campaign/{id}/banner": { - "patch": { - "operationId": "updateCampaignBannerById", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Campaign ID", - "required": true, + "delete": { + "operationId": "deleteOrganisationMemberById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "description": "Updates banner image for specified campaign.", - "tags": [ - "Campaign" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "properties": { - "upload_url": { - "type": "string", - "description": "Presigned S3 url to upload file.", - "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJJWZ7B6WCRGMKFGQ%2F20180210%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20180210T171315Z&X-Amz-Expires=1800&X-Amz-Signature=12b74b0788aa036bc7c3d03b3f20c61f1f91cc9ad8873e3314255dc479a25351&X-Amz-SignedHeaders=host" - } + "properties": { + "user_id": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "description": "Specifies member for deletion in organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." } } } } - }, - "401": { - "description": "Not logged in.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Not logged in." - } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" } } } } - }, - "403": { - "description": "User is not an organisation admin.", - "content": { - "application/json": { - "schema": { - "properties": { - "error": { - "type": "string", - "example": "Unauthorized" + } + } + } + } + }, + "/organisation/{id}/admin": { + "get": { + "operationId": "getOrganisationAdminsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns list of admins of specified organisation.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "role": { + "type": "string", + "example": "Admin" + } + } } } } } } } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateOrganisationAdminsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "members": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer", + "format": "int64" + }, + "example": [ + 1541815603606036500, 1541815603606036700, + 1541815287306036500 + ] + } + } + } + } + } + }, + "description": "Specifies Admins for specified organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated members." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteOrganisationAdminById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Organisation ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "user_id": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "description": "Specifies Admin for deletion in organistion.", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted Admin." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a SuperUser.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign": { + "get": { + "operationId": "getAllCampaigns", + "description": "Returns all active campaigns.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036700 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + } + } + } + } + } + } + }, + "/campaign/{id}": { + "get": { + "operationId": "getCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036700 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + } + } + } + }, + "description": "Updates details of specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated campaign." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteCampaignById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted campaign." + } + } + } + } + } + }, + "403": { + "description": "User is not an admin of campaign's organisation.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/banner": { + "patch": { + "operationId": "updateCampaignBannerById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Updates banner image for specified campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "upload_url": { + "type": "string", + "description": "Presigned S3 url to upload file.", + "example": "https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d" + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an organisation admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/role": { + "post": { + "operationId": "createRole", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Creates a new role in a campaign.", + "tags": ["Campaign"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "required": false, + "example": "Larry the cat is dead, now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully created organisation." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/roles": { + "get": { + "operationId": "getRolesByCampaignId", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about all roles in a campaign", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "campaigns": { + "type": "array", + "items": { + "$ref": "#components/schemas/RoleDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/campaign/{id}/applications": { + "get": { + "operationId": "getApplicationsById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "campaign ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about all Applications in given Campaign.", + "tags": ["Campaign"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/role/{id}": { + "get": { + "operationId": "getRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns info about specified role.", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "example": "Larry the cat gone missing! now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + } + } + }, + "put": { + "operationId": "updateRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Update a role given the role id.", + "tags": ["Role"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "Chief Whip" + }, + "description": { + "type": "string", + "required": false, + "example": "Put a bit of stick about!" + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully update organisation." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not a Campaign Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "deleteRoleById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Deletes specified role.", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted role." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an admin of role's Campaign.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/role/{id}/applications": { + "get": { + "operationId": "getApplicationsByRoleID", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Role ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns all applications to a specific role", + "tags": ["Role"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}": { + "get": { + "operationId": "getApplicationByID", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "description": "Returns an applications given its ID", + "tags": ["Application"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "application": { + "type": { + "$ref": "#components/schemas/ApplicationDetails" + } + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}/private": { + "put": { + "operationId": "updateApplicationPrivateStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": { + "$ref": "#components/schemas/ApplicationStatus" + } + } + } + } + } + } + }, + "description": "Change Private Status of a specific Application", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated Application Private Status." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + }, + "/application/{id}/status": { + "put": { + "operationId": "updateApplicationStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Application ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": { + "$ref": "#components/schemas/ApplicationStatus" + } + } + } + } + } + } + }, + "description": "Change Status of a specific Application", + "tags": ["Organisation"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Successfully updated Application Status." + } + } + } + } + } + }, + "401": { + "description": "Not logged in.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Not logged in." + } + } + } + } + } + }, + "403": { + "description": "User is not an Application Admin.", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NotLoggedIn": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Not logged in" + } + } + }, + "Unauthorized": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + }, + "BadRequest": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Bad request" + } + } + }, + "Answer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965227000 + }, + "question_id": { + "type": "integer", + "format": "int64", + "example": 6996987893965227000 + }, + "answer_type": { + "type": "string", + "enum": [ + "ShortAnswer", + "MultiChoice", + "MultiSelect", + "DropDown", + "Ranking" + ] + }, + "data": { + "oneOf": [ + { + "type": "string", + "example": "I am passionate about events" + }, + { + "type": "integer", + "example": 6996987893965227000 + }, + { + "type": "array", + "format": "int64", + "example": [ + 6996987893965227000, 69969829832652230000, 6996987893965228000 + ] + } + ] + }, + "created_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "updated_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + } + } + }, + "NewAnswer": { + "type": "object", + "properties": { + "question_id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "answer_type": { + "type": "string", + "enum": [ + "ShortAnswer", + "MultiChoice", + "MultiSelect", + "DropDown", + "Ranking" + ] + }, + "data": { + "oneOf": [ + { + "type": "string", + "example": "I am passionate about events" + }, + { + "type": "integer", + "example": 6996987893965227000 + }, + { + "type": "array", + "format": "int64", + "example": [ + 6996987893965227000, 69969829832652230000, 6996987893965228000 + ] + } + ] + } + } + }, + "ApplicationStatus": { + "type": "string", + "enum": ["Pending", "Rejected", "Successful"] + }, + "OrganisationDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "slug": { + "type": "string", + "example": "devsoc-unsw" + }, + "name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "logo": { + "type": "string", + "example": "76718252-2a13-4de2-bc07-f977c75dc52b" + }, + "created_at": { + "type": "string", + "example": "2024-02-10T18:25:43.511Z" + } + } + }, + "OrganisationCampaign": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 6996987893965263000 + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "cover_image": { + "type": "string", + "example": "2d19617b-46fd-4927-9f53-77d69232ba5d" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + }, + "RoleDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 7036987893965263000 + }, + "campaign_id": { + "type": "integer", + "format": "int64", + "example": 1116987453965262800 + }, + "name": { + "type": "string", + "example": "Chief Mouser" + }, + "description": { + "type": "string", + "example": "Larry the cat gone missing! now we need someone else to handle the rat issues at 10th Downing st." + }, + "min_available": { + "type": "int32", + "example": 1 + }, + "max_available": { + "type": "int32", + "example": 3 + }, + "finalised": { + "type": "boolean", + "description": "Whether this role has been finalised (e.g. max avaliable number)", + "example": false + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "email": { + "type": "string", + "example": "me@example.com" + }, + "zid": { + "type": "string", + "example": "z5555555", + "nullable": true + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "pronouns": { + "type": "string", + "example": "He/Him" + }, + "gender": { + "type": "string", + "example": "Male" + }, + "degree_name": { + "type": "string", + "example": "Computer Science", + "nullable": true + }, + "degree_starting_year": { + "type": "integer", + "example": 2024, + "nullable": true + }, + "role": { + "type": "string", + "enum": ["User", "SuperUser"] + } + } + }, + "UserDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "email": { + "type": "string", + "example": "me@example.com" + }, + "zid": { + "type": "string", + "example": "z5555555" + }, + "name": { + "type": "string", + "example": "Clancy Lion" + }, + "pronouns": { + "type": "string", + "example": "They/Them" + }, + "gender": { + "type": "string", + "example": "Male" + }, + "degree_name": { + "type": "string", + "example": "Computer Science" + }, + "degree_starting_year": { + "type": "integer", + "example": 2024 + } + } + }, + "ApplicationDetails": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "campaign_id": { + "type": "integer", + "format": "int64", + "example": 5141815603606036000 + }, + "user": { + "$ref": "#/components/schemas/UserDetails" + }, + "status": { + "$ref": "#/components/schemas/ApplicationStatus" + }, + "private_status": { + "$ref": "#/components/schemas/ApplicationStatus" + }, + "applied_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAppliedRoleDetails" + } + } + } + }, + "ApplicationAppliedRoleDetails": { + "type": "object", + "properties": { + "campaign_role_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "role_name": { + "type": "String", + "example": "Sponsorships" + }, + "preference": { + "type": "integer", + "format": "int32", + "example": 1 + } + } + }, + "NewCampaign": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?" + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + } + } + }, + "Campaign": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "slug": { + "type": "string", + "example": "2024-subcom-recruitment" + }, + "name": { + "type": "string", + "example": "2024 Subcommittee Recruitment" + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "organisation_name": { + "type": "string", + "example": "UNSW Software Development Society" + }, + "cover_image": { + "type": "string", + "format": "uuid", + "example": "05ebad1e-8be4-40c3-9d36-140cac9a0075", + "nullable": true + }, + "description": { + "type": "string", + "example": "Are you excited to make a difference?", + "nullable": true + }, + "starts_at": { + "type": "string", + "example": "2024-03-15T18:25:43.511Z" + }, + "ends_at": { + "type": "string", + "example": "2024-04-15T18:25:43.511Z" + }, + "created_at": { + "type": "string", + "example": "2024-02-15T18:25:43.511Z" + }, + "updated_at": { + "type": "string", + "example": "2024-02-15T18:25:43.511Z" + } + } + }, + "EmailTemplate": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "organisation_id": { + "type": "integer", + "format": "int64", + "example": 1541815603606036500 + }, + "name": { + "type": "string", + "example": "Success Email" + }, + "template_subject": { + "type": "string", + "example": "[OUTCOME] {{campaign_name}} - {{role_name}}" + }, + "template_body": { + "type": "string", + "example": "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + } + } + }, + "NewEmailTemplate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Success Email" + }, + "template_subject": { + "type": "string", + "example": "[OUTCOME] {{campaign_name}} - {{role_name}}" + }, + "template_body": { + "type": "string", + "example": "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + } + } + } + }, + "responses": { + "NotLoggedIn": { + "description": "Redirect to login", + "headers": { + "Location": { + "description": "Login url", + "schema": { + "type": "string", + "format": "uri" + } + } + } + }, + "NotOrganisationAdmin": { + "description": "User is not an organisation admin", + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + } + } } } } } - } \ No newline at end of file + } +} diff --git a/backend/api.yaml b/backend/api.yaml index 729d4030..d0684e4b 100644 --- a/backend/api.yaml +++ b/backend/api.yaml @@ -9,158 +9,80 @@ servers: description: Local server paths: - /auth/logout: - post: - operationId: logout - description: Invalidates current token. + /: + get: + operationId: getRoot + description: Root of API tags: - - Auth + - Miscellaneous responses: - '200': + "200": description: OK content: - application/json: + text/plain: schema: - properties: - messages: - type: string - example: Successfully logged out. - /user: + type: string + example: Join DevSoc! https://devsoc.app/ + + /auth/callback/google: get: - operationId: getLoggedInUser - description: Returns info about currently logged in user. + operationId: googleCallback + description: Google OAuth callback tags: - - User + - Auth + parameters: + - name: code + in: query + description: Google OAuth code + required: true + schema: + type: string responses: - '200': - description: OK - content: - application/json: - schema: - properties: - id: - type: integer - format: int64 - example: 1541815603606036480 - email: - type: string - example: me@example.com - zid: - type: string - example: z5555555 - name: - type: string - example: Clancy Lion - degree_name: - type: string - example: Computer Science - degree_starting_year: - type: integer - example: 2024 - role: - type: string - example: User - '401': - description: Not logged in. + "200": + description: Ok content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - delete: - operationId: deleteUserById - description: Deletes currently logged in user. + /auth/logout: + post: + operationId: logout + description: Invalidates current token tags: - - User + - Auth responses: - '200': - description: OK + "200": + description: Ok content: application/json: schema: properties: message: type: string - example: Successfully deleted user. - '403': - description: User is only admin of an organisation. + example: Successfully logged out + "401": + description: Not logged in content: application/json: schema: - properties: - error: - type: string - example: "Cannot delete sole admin of an organisation." - '401': - description: Not logged. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - /user/{id}: + $ref: "#/components/schemas/NotLoggedIn" + + /user: get: - operationId: getUserById - parameters: - - name: id - in: path - description: User ID - required: true - schema: - type: integer - format: int64 - description: Returns info about specified user. + operationId: getLoggedInUser + description: Returns info about currently logged in user tags: - User responses: - '200': + "200": description: OK content: application/json: schema: - properties: - id: - type: integer - format: int64 - example: 1541815603606036480 - email: - type: string - example: me@example.com - zid: - type: string - example: z5555555 - name: - type: string - example: Clancy Lion - pronouns: - type: string - example: They/Them - gender: - type: string - example: Male - degree_name: - type: string - example: Computer Science - degree_starting_year: - type: integer - example: 2024 - '403': - description: Requested user has not applied to one of authorized user's campaign. - content: - application/json: - schema: - properties: - error: - type: string - example: "Insufficient permissions" + $ref: "#/components/schemas/User" + "307": + $ref: "#/components/responses/NotLoggedIn" /user/name: patch: operationId: updateUserName - description: Updates currently logged in user's name. + description: Updates currently logged in user's name tags: - User requestBody: @@ -173,7 +95,7 @@ paths: type: string example: "Clancy Tiger" responses: - '200': + "200": description: OK content: application/json: @@ -181,21 +103,13 @@ paths: properties: message: type: string - example: Successfully updated name. - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - + example: Successfully updated name + "307": + $ref: "#/components/responses/NotLoggedIn" /user/pronouns: patch: operationId: updateUserPronouns - description: Updates currently logged in user's pronouns. + description: Updates currently logged in user's pronouns tags: - User requestBody: @@ -204,11 +118,11 @@ paths: application/json: schema: properties: - zid: + pronouns: type: string - example: "z5123456" + example: They/Them responses: - '200': + "200": description: OK content: application/json: @@ -216,20 +130,13 @@ paths: properties: message: type: string - example: Successfully updated pronouns. - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. + example: Successfully updated pronouns + "307": + $ref: "#/components/responses/NotLoggedIn" /user/gender: patch: operationId: updateUserGender - description: Updates currently logged in user's gender. + description: Updates currently logged in user's gender tags: - User requestBody: @@ -238,11 +145,11 @@ paths: application/json: schema: properties: - zid: + gender: type: string - example: "z5123456" + example: Female responses: - '200': + "200": description: OK content: application/json: @@ -250,22 +157,13 @@ paths: properties: message: type: string - example: Successfully updated gender. - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - - + example: Successfully updated gender + "307": + $ref: "#/components/responses/NotLoggedIn" /user/zid: patch: operationId: updateUserZid - description: Updates currently logged in user's zID. + description: Updates currently logged in user's zID tags: - User requestBody: @@ -276,9 +174,9 @@ paths: properties: zid: type: string - example: "z5123456" + example: z5123456 responses: - '200': + "200": description: OK content: application/json: @@ -286,20 +184,13 @@ paths: properties: message: type: string - example: Successfully updated zID. - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. + example: Successfully updated zID + "307": + $ref: "#/components/responses/NotLoggedIn" /user/degree: patch: operationId: updateUserDegree - description: Updates currently logged in user's degree. + description: Updates currently logged in user's degree tags: - User requestBody: @@ -310,12 +201,12 @@ paths: properties: degree_name: type: string - example: "Electrical Engineering" + example: Electrical Engineering degree_starting_year: type: integer example: 2024 responses: - '200': + "200": description: OK content: application/json: @@ -323,25 +214,17 @@ paths: properties: message: type: string - example: Successfully updated email. - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - + example: Successfully updated email + "307": + $ref: "#/components/responses/NotLoggedIn" /user/applications: get: operationId: getUserApplications - description: Returns info about applications made by currently logged in user. + description: Returns info about applications made by currently logged in user tags: - User responses: - '200': + "200": description: OK content: application/json: @@ -350,23 +233,14 @@ paths: campaigns: type: array items: - $ref: '#/components/schemas/ApplicationDetails' - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - - + $ref: "#/components/schemas/ApplicationDetails" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation: post: operationId: createOrganisation - description: Creates a new organisation. + description: Create a new organisation tags: - Organisation requestBody: @@ -375,16 +249,20 @@ paths: application/json: schema: properties: + slug: + type: string + description: ASCII string for URL like https://chaos.csesoc.app/s/unsw-devsoc + example: unsw-devsoc name: type: string - example: "UNSW Software Development Society" + example: UNSW Software Development Society admin: type: integer format: int64 example: 1541815603606036480 description: User ID of admin responses: - '200': + "200": description: OK content: application/json: @@ -392,16 +270,48 @@ paths: properties: message: type: string - example: Successfully created organisation. - '403': - description: User is not a super user. + example: Successfully created organisation + "403": + description: User is not a SuperUser + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/slug_check: + post: + operationId: checkOrganisationSlugAvailability + description: Check if slug is available + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + properties: + slug: + type: string + example: unsw-devsoc + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Unauthorized + example: Organisation slug is available + "400": + description: Bad request - slug is in use or not ASCII + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequest" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}: get: operationId: getOrganisationById @@ -413,38 +323,18 @@ paths: schema: type: integer format: int64 - description: Returns info about specified organisation. + description: Returns info about specified organisation tags: - Organisation responses: - '200': + "200": description: OK content: application/json: schema: - properties: - id: - type: integer - format: int64 - example: 6996987893965262849 - name: - type: string - example: UNSW Software Development Society - logo: - type: string - example: "76718252-2a13-4de2-bc07-f977c75dc52b" - created_at: - type: string - example: 2024-02-10T18:25:43.511Z - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. + $ref: "#/components/schemas/OrganisationDetails" + "307": + $ref: "#/components/responses/NotLoggedIn" delete: operationId: deleteOrganisationById parameters: @@ -455,11 +345,11 @@ paths: schema: type: integer format: int64 - description: Deletes specified organisation. + description: Deletes specified organisation tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -467,28 +357,111 @@ paths: properties: message: type: string - example: Successfully deleted organisation. - '401': - description: Not logged in. + example: Successfully deleted organisation + "403": + description: User is not a SuperUser + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/slug/{slug}: + get: + operationId: getOrganisationBySlug + parameters: + - name: slug + in: path + description: Organisation slug + required: true + schema: + type: string + description: Returns info about specified organisation + tags: + - Organisation + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/OrganisationDetails" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/campaign: + post: + operationId: createCampaign + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Create a new campaign inside specified organisation + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewCampaign" + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Not logged in. - '403': - description: User is not a super user. + example: Successfully created campaign + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/campaign/slug_check: + post: + operationId: checkCampaignSlugAvailability + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Checks availability of campaign slug in specified organisation + tags: + - Organisation + requestBody: + required: true + content: + application/json: + schema: + properties: + slug: + type: string + example: 2024-subcom-recruitment + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Unauthorized + example: Campaign slug is available + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/campaigns: get: - operationId: getOrganisationCampaignsById + operationId: getAllOrganisationCampaigns parameters: - name: id in: path @@ -497,11 +470,11 @@ paths: schema: type: integer format: int64 - description: Returns active campaigns for specified organisation. + description: Returns all (active & ended) campaigns for specified organisation. However, ended campaigns cannot have new applications tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -510,17 +483,69 @@ paths: campaigns: type: array items: - $ref: '#/components/schemas/OrganisationCampaign' - '401': - description: Not logged in. + $ref: "#/components/schemas/OrganisationCampaign" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/email_template: + post: + operationId: createEmailTemplate + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Create a new email template within the organisation + tags: + - Organisation + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewEmailTemplate" + responses: + "200": + description: OK content: application/json: schema: properties: - error: + message: type: string - example: Not logged in. - + example: Successfully created email template + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" + /organisation/{id}/email_templates: + get: + operationId: getAllOrganisationEmailTemplates + parameters: + - name: id + in: path + description: Organisation ID + required: true + schema: + type: integer + format: int64 + description: Get all email templates for specified organisation + tags: + - Organisation + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EmailTemplate" + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/logo: patch: operationId: updateOrganisationLogoById @@ -532,11 +557,11 @@ paths: schema: type: integer format: int64 - description: Updates logo for specified organistion. + description: Update logo for specified organistion. Returns a PUT url to upload new image to tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -544,27 +569,12 @@ paths: properties: upload_url: type: string - description: Presigned S3 url to upload file. - example: https://www.youtube.com/watch?v=dQw4w9WgXcQ - '401': - description: Not logged in. - content: - application/json: - schema: - properties: - error: - type: string - example: Not logged in. - '403': - description: User is not an organisation admin. - content: - application/json: - schema: - properties: - error: - type: string - example: Unauthorized - + description: Presigned S3 url to upload file + example: https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d + "401": + $ref: "#/components/responses/NotOrganisationAdmin" + "307": + $ref: "#/components/responses/NotLoggedIn" /organisation/{id}/member: get: operationId: getOrganisationMembersById @@ -580,7 +590,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -601,7 +611,7 @@ paths: role: type: string example: Admin - '403': + "403": description: User is not an organisation admin or member. content: application/json: @@ -632,12 +642,17 @@ paths: items: type: integer format: int64 - example: [1541815603606036480, 1541815603606036827, 1541815287306036429] + example: + [ + 1541815603606036480, + 1541815603606036827, + 1541815287306036429, + ] description: Specifies members for specified organistion. tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -646,7 +661,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -655,7 +670,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -687,7 +702,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -696,7 +711,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -705,7 +720,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -730,7 +745,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -751,7 +766,7 @@ paths: role: type: string example: Admin - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -782,12 +797,17 @@ paths: items: type: integer format: int64 - example: [1541815603606036480, 1541815603606036827, 1541815287306036429] + example: + [ + 1541815603606036480, + 1541815603606036827, + 1541815287306036429, + ] description: Specifies Admins for specified organistion. tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -796,7 +816,7 @@ paths: message: type: string example: Successfully updated members. - '401': + "401": description: Not logged in. content: application/json: @@ -805,7 +825,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -837,7 +857,7 @@ paths: tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -846,7 +866,7 @@ paths: message: type: string example: Successfully deleted Admin. - '401': + "401": description: Not logged in. content: application/json: @@ -855,7 +875,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a SuperUser. content: application/json: @@ -864,58 +884,6 @@ paths: error: type: string example: Unauthorized - - /organisation/{id}/campaign: - post: - operationId: createCampaign - parameters: - - name: id - in: path - description: Organisation ID - required: true - schema: - type: integer - format: int64 - description: Creates a new campaign inside specified organisation. - tags: - - Organisation - requestBody: - required: true - content: - application/json: - schema: - properties: - name: - type: string - example: 2024 Subcommittee Recruitment - description: - type: string - example: Are you excited to make a difference? - starts_at: - type: string - example: 2024-03-15T18:25:43.511Z - ends_at: - type: string - example: 2024-04-15T18:25:43.511Z - responses: - '200': - description: OK - content: - application/json: - schema: - properties: - message: - type: string - example: Successfully created campaign. - '403': - description: User is not an admin of specified organisation. - content: - application/json: - schema: - properties: - error: - type: string - example: Unauthorized /campaign: get: operationId: getAllCampaigns @@ -923,7 +891,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -975,7 +943,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1039,7 +1007,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1048,7 +1016,7 @@ paths: message: type: string example: Successfully updated campaign. - '401': + "401": description: Not logged in. content: application/json: @@ -1057,7 +1025,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -1080,7 +1048,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1089,7 +1057,7 @@ paths: message: type: string example: Successfully deleted campaign. - '403': + "403": description: User is not an admin of campaign's organisation. content: application/json: @@ -1113,7 +1081,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1122,8 +1090,8 @@ paths: upload_url: type: string description: Presigned S3 url to upload file. - example: https://www.youtube.com/watch?v=dQw4w9WgXcQ - '401': + example: https://presignedurldemo.s3.eu-west-2.amazonaws.com/6996987893965262849/2d19617b-46fd-4927-9f53-77d69232ba5d + "401": description: Not logged in. content: application/json: @@ -1132,7 +1100,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an organisation admin. content: application/json: @@ -1180,7 +1148,7 @@ paths: description: Whether this role has been finalised (e.g. max avaliable number) example: False responses: - '200': + "200": description: OK content: application/json: @@ -1189,7 +1157,7 @@ paths: message: type: string example: Successfully created organisation. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1214,7 +1182,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1223,8 +1191,8 @@ paths: campaigns: type: array items: - $ref: '#components/schemas/RoleDetails' - '401': + $ref: "#components/schemas/RoleDetails" + "401": description: Not logged in. content: application/json: @@ -1233,7 +1201,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1258,7 +1226,7 @@ paths: tags: - Campaign responses: - '200': + "200": description: OK content: application/json: @@ -1267,8 +1235,8 @@ paths: applications: type: array items: - $ref: '#components/schemas/ApplicationDetails' - '401': + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1277,7 +1245,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1287,7 +1255,6 @@ paths: type: string example: Unauthorized - /role/{id}: get: operationId: getRoleById @@ -1303,7 +1270,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1325,7 +1292,7 @@ paths: type: boolean description: Whether this role has been finalised (e.g. max avaliable number) example: False - '401': + "401": description: Not logged in. content: application/json: @@ -1372,7 +1339,7 @@ paths: description: Whether this role has been finalised (e.g. max avaliable number) example: true responses: - '200': + "200": description: OK content: application/json: @@ -1381,7 +1348,7 @@ paths: message: type: string example: Successfully update organisation. - '401': + "401": description: Not logged in. content: application/json: @@ -1390,7 +1357,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not a Campaign Admin. content: application/json: @@ -1414,7 +1381,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1423,7 +1390,7 @@ paths: message: type: string example: Successfully deleted role. - '401': + "401": description: Not logged in. content: application/json: @@ -1432,7 +1399,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an admin of role's Campaign. content: application/json: @@ -1457,7 +1424,7 @@ paths: tags: - Role responses: - '200': + "200": description: OK content: application/json: @@ -1466,8 +1433,8 @@ paths: applications: type: array items: - $ref: '#components/schemas/ApplicationDetails' - '401': + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1476,7 +1443,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1501,16 +1468,16 @@ paths: tags: - Application responses: - '200': + "200": description: OK content: application/json: schema: properties: application: - type: - $ref: '#components/schemas/ApplicationDetails' - '401': + type: + $ref: "#components/schemas/ApplicationDetails" + "401": description: Not logged in. content: application/json: @@ -1519,7 +1486,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1529,7 +1496,6 @@ paths: type: string example: Unauthorized - /application/{id}/private: put: operationId: updateApplicationPrivateStatus @@ -1549,13 +1515,13 @@ paths: properties: data: type: - $ref: '#components/schemas/ApplicationStatus' + $ref: "#components/schemas/ApplicationStatus" description: Change Private Status of a specific Application tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -1564,7 +1530,7 @@ paths: message: type: string example: Successfully updated Application Private Status. - '401': + "401": description: Not logged in. content: application/json: @@ -1573,7 +1539,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1602,13 +1568,13 @@ paths: properties: data: type: - $ref: '#components/schemas/ApplicationStatus' + $ref: "#components/schemas/ApplicationStatus" description: Change Status of a specific Application tags: - Organisation responses: - '200': + "200": description: OK content: application/json: @@ -1617,7 +1583,7 @@ paths: message: type: string example: Successfully updated Application Status. - '401': + "401": description: Not logged in. content: application/json: @@ -1626,7 +1592,7 @@ paths: error: type: string example: Not logged in. - '403': + "403": description: User is not an Application Admin. content: application/json: @@ -1638,6 +1604,89 @@ paths: components: schemas: + NotLoggedIn: + type: object + properties: + message: + type: string + example: Not logged in + + Unauthorized: + type: object + properties: + message: + type: string + example: Unauthorized + + BadRequest: + type: object + properties: + message: + type: string + example: Bad request + + Answer: + type: object + properties: + id: + type: integer + format: int64 + example: 6996987893965227483 + question_id: + type: integer + format: int64 + example: 6996987893965227483 + answer_type: + type: string + enum: + - ShortAnswer + - MultiChoice + - MultiSelect + - DropDown + - Ranking + data: + oneOf: + - type: string + example: I am passionate about events + - type: integer + example: 6996987893965227483 + - type: array + format: int64 + example: + [6996987893965227483, 69969829832652228374, 6996987893965228374] + created_at: + type: string + example: 2024-03-15T18:25:43.511Z + updated_at: + type: string + example: 2024-03-15T18:25:43.511Z + + NewAnswer: + type: object + properties: + question_id: + type: integer + format: int64 + example: 6996987893965262849 + answer_type: + type: string + enum: + - ShortAnswer + - MultiChoice + - MultiSelect + - DropDown + - Ranking + data: + oneOf: + - type: string + example: I am passionate about events + - type: integer + example: 6996987893965227483 + - type: array + format: int64 + example: + [6996987893965227483, 69969829832652228374, 6996987893965228374] + ApplicationStatus: type: string enum: @@ -1645,6 +1694,26 @@ components: - Rejected - Successful + OrganisationDetails: + type: object + properties: + id: + type: integer + format: int64 + example: 6996987893965262849 + slug: + type: string + example: devsoc-unsw + name: + type: string + example: UNSW Software Development Society + logo: + type: string + example: "76718252-2a13-4de2-bc07-f977c75dc52b" + created_at: + type: string + example: 2024-02-10T18:25:43.511Z + OrganisationCampaign: type: object properties: @@ -1652,6 +1721,9 @@ components: type: integer format: int64 example: 6996987893965262849 + slug: + type: string + example: 2024-subcom-recruitment name: type: string example: 2024 Subcommittee Recruitment @@ -1696,6 +1768,43 @@ components: description: Whether this role has been finalised (e.g. max avaliable number) example: False + User: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + email: + type: string + example: me@example.com + zid: + type: string + example: z5555555 + nullable: true + name: + type: string + example: Clancy Lion + pronouns: + type: string + example: He/Him + gender: + type: string + example: Male + degree_name: + type: string + example: Computer Science + nullable: true + degree_starting_year: + type: integer + example: 2024 + nullable: true + role: + type: string + enum: + - User + - SuperUser + UserDetails: type: object properties: @@ -1737,15 +1846,15 @@ components: format: int64 example: 5141815603606036480 user: - $ref: '#/components/schemas/UserDetails' + $ref: "#/components/schemas/UserDetails" status: - $ref: '#/components/schemas/ApplicationStatus' + $ref: "#/components/schemas/ApplicationStatus" private_status: - $ref: '#/components/schemas/ApplicationStatus' + $ref: "#/components/schemas/ApplicationStatus" applied_roles: type: array items: - $ref: '#/components/schemas/ApplicationAppliedRoleDetails' + $ref: "#/components/schemas/ApplicationAppliedRoleDetails" ApplicationAppliedRoleDetails: type: object @@ -1756,4 +1865,123 @@ components: example: 1541815603606036480 role_name: type: String - example: UI/UX subcom \ No newline at end of file + example: Sponsorships + preference: + type: integer + format: int32 + example: 1 + + NewCampaign: + type: object + properties: + name: + type: string + example: 2024 Subcommittee Recruitment + slug: + type: string + example: 2024-subcom-recruitment + description: + type: string + example: Are you excited to make a difference? + starts_at: + type: string + example: 2024-03-15T18:25:43.511Z + ends_at: + type: string + example: 2024-04-15T18:25:43.511Z + + Campaign: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + slug: + type: string + example: 2024-subcom-recruitment + name: + type: string + example: 2024 Subcommittee Recruitment + organisation_id: + type: integer + format: int64 + example: 1541815603606036480 + organisation_name: + type: string + example: UNSW Software Development Society + cover_image: + type: string + format: uuid + example: 05ebad1e-8be4-40c3-9d36-140cac9a0075 + nullable: true + description: + type: string + example: Are you excited to make a difference? + nullable: true + starts_at: + type: string + example: 2024-03-15T18:25:43.511Z + ends_at: + type: string + example: 2024-04-15T18:25:43.511Z + created_at: + type: string + example: 2024-02-15T18:25:43.511Z + updated_at: + type: string + example: 2024-02-15T18:25:43.511Z + + EmailTemplate: + type: object + properties: + id: + type: integer + format: int64 + example: 1541815603606036480 + organisation_id: + type: integer + format: int64 + example: 1541815603606036480 + name: + type: string + example: Success Email + template_subject: + type: string + example: "[OUTCOME] {{campaign_name}} - {{role_name}}" + template_body: + type: string + example: "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + + NewEmailTemplate: + type: object + properties: + name: + type: string + example: Success Email + template_subject: + type: string + example: "[OUTCOME] {{campaign_name}} - {{role_name}}" + template_body: + type: string + example: "Hi {{name}}, you have been successful in your application for {{organisation_name}} {{role_name}}. Regards {{organisation_name}} Exec" + + responses: + NotLoggedIn: + description: Redirect to login + headers: + Location: + description: Login url + schema: + type: string + format: uri + + NotOrganisationAdmin: + description: User is not an organisation admin + content: + application/json: + schema: + properties: + error: + type: string + example: Unauthorized diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index 01ee6439..cca4c04f 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -8,6 +8,7 @@ CREATE TABLE applications ( private_status application_status NOT NULL DEFAULT 'Pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + submitted BOOLEAN NOT NULL DEFAULT false, CONSTRAINT FK_applications_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -24,6 +25,7 @@ CREATE TABLE application_roles ( id BIGSERIAL PRIMARY KEY, application_id BIGINT NOT NULL, campaign_role_id BIGINT NOT NULL, + preference INTEGER NOT NULL, CONSTRAINT FK_application_roles_applications FOREIGN KEY(application_id) REFERENCES applications(id) diff --git a/backend/migrations/20241124054711_email_templates.sql b/backend/migrations/20241124054711_email_templates.sql index 393123c9..ff1a4879 100644 --- a/backend/migrations/20241124054711_email_templates.sql +++ b/backend/migrations/20241124054711_email_templates.sql @@ -2,7 +2,8 @@ CREATE TABLE email_templates ( id BIGINT PRIMARY KEY, organisation_id BIGINT NOT NULL, name TEXT NOT NULL, - template TEXT NOT NULL, + template_subject TEXT NOT NULL, + template_body TEXT NOT NULL, CONSTRAINT FK_email_templates_organisations FOREIGN KEY(organisation_id) REFERENCES organisations(id) diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index de3d9bb6..f3e4c07f 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -27,3 +27,4 @@ rs-snowflake = "0.6" jsonwebtoken = "9.1" dotenvy = "0.15" handlebars = "6.2" +lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder"] } diff --git a/backend/server/src/handler/answer.rs b/backend/server/src/handler/answer.rs index 065be1da..102e586b 100644 --- a/backend/server/src/handler/answer.rs +++ b/backend/server/src/handler/answer.rs @@ -7,6 +7,7 @@ use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; use serde_json::json; +use crate::models::application::{OpenApplicationByAnswerId, OpenApplicationByApplicationId}; pub struct AnswerHandler; @@ -15,6 +16,7 @@ impl AnswerHandler { State(state): State, Path(application_id): Path, _user: ApplicationOwner, + _: OpenApplicationByApplicationId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -63,6 +65,7 @@ impl AnswerHandler { pub async fn update( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { @@ -76,6 +79,7 @@ impl AnswerHandler { pub async fn delete( Path(answer_id): Path, _owner: AnswerOwner, + _: OpenApplicationByAnswerId, mut transaction: DBTransaction<'_>, ) -> Result { Answer::delete(answer_id, &mut transaction.tx).await?; diff --git a/backend/server/src/handler/application.rs b/backend/server/src/handler/application.rs index 8f6c2ecf..00afd7cf 100644 --- a/backend/server/src/handler/application.rs +++ b/backend/server/src/handler/application.rs @@ -1,6 +1,6 @@ use crate::models::app::AppState; -use crate::models::application::{Application, ApplicationStatus}; -use crate::models::auth::{ApplicationAdmin, AuthUser}; +use crate::models::application::{Application, ApplicationRoleUpdate, ApplicationStatus, OpenApplicationByApplicationId}; +use crate::models::auth::{ApplicationAdmin, ApplicationOwner, AuthUser}; use crate::models::error::ChaosError; use crate::models::transaction::DBTransaction; use axum::extract::{Json, Path, State}; @@ -48,4 +48,26 @@ impl ApplicationHandler { transaction.tx.commit().await?; Ok((StatusCode::OK, Json(applications))) } + + pub async fn update_roles( + _user: ApplicationOwner, + Path(application_id): Path, + mut transaction: DBTransaction<'_>, + Json(data): Json, + ) -> Result { + Application::update_roles(application_id, data.roles, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully updated application roles")) + } + + pub async fn submit( + _user: ApplicationOwner, + _: OpenApplicationByApplicationId, + Path(application_id): Path, + mut transaction: DBTransaction<'_>, + ) -> Result { + Application::submit(application_id, &mut transaction.tx).await?; + transaction.tx.commit().await?; + Ok((StatusCode::OK, "Successfully submitted application")) + } } diff --git a/backend/server/src/handler/campaign.rs b/backend/server/src/handler/campaign.rs index 0e1db393..49671ad8 100644 --- a/backend/server/src/handler/campaign.rs +++ b/backend/server/src/handler/campaign.rs @@ -4,7 +4,7 @@ use crate::models::application::Application; use crate::models::application::NewApplication; use crate::models::auth::AuthUser; use crate::models::auth::CampaignAdmin; -use crate::models::campaign::Campaign; +use crate::models::campaign::{Campaign, OpenCampaign}; use crate::models::error::ChaosError; use crate::models::offer::Offer; use crate::models::role::{Role, RoleUpdate}; @@ -104,6 +104,7 @@ impl CampaignHandler { State(state): State, Path(id): Path, user: AuthUser, + _: OpenCampaign, mut transaction: DBTransaction<'_>, Json(data): Json, ) -> Result { diff --git a/backend/server/src/handler/email_template.rs b/backend/server/src/handler/email_template.rs index e392834f..4150ac98 100644 --- a/backend/server/src/handler/email_template.rs +++ b/backend/server/src/handler/email_template.rs @@ -25,7 +25,14 @@ impl EmailTemplateHandler { State(state): State, Json(request_body): Json, ) -> Result { - EmailTemplate::update(id, request_body.name, request_body.template, &state.db).await?; + EmailTemplate::update( + id, + request_body.name, + request_body.template_subject, + request_body.template_body, + &state.db, + ) + .await?; Ok((StatusCode::OK, "Successfully updated email template")) } diff --git a/backend/server/src/handler/offer.rs b/backend/server/src/handler/offer.rs index 92297c63..32e4851c 100644 --- a/backend/server/src/handler/offer.rs +++ b/backend/server/src/handler/offer.rs @@ -1,8 +1,9 @@ +use crate::models::app::AppState; use crate::models::auth::{OfferAdmin, OfferRecipient}; use crate::models::error::ChaosError; use crate::models::offer::{Offer, OfferReply}; use crate::models::transaction::DBTransaction; -use axum::extract::{Json, Path}; +use axum::extract::{Json, Path, State}; use axum::http::StatusCode; use axum::response::IntoResponse; @@ -47,18 +48,19 @@ impl OfferHandler { Path(id): Path, _user: OfferAdmin, ) -> Result { - let string = Offer::preview_email(id, &mut transaction.tx).await?; + let email_parts = Offer::preview_email(id, &mut transaction.tx).await?; transaction.tx.commit().await?; - Ok((StatusCode::OK, string)) + Ok((StatusCode::OK, Json(email_parts))) } pub async fn send_offer( mut transaction: DBTransaction<'_>, Path(id): Path, _user: OfferAdmin, + State(state): State, ) -> Result { - Offer::send_offer(id, &mut transaction.tx).await?; + Offer::send_offer(id, &mut transaction.tx, state.email_credentials).await?; transaction.tx.commit().await?; Ok((StatusCode::OK, "Successfully sent offer")) diff --git a/backend/server/src/handler/organisation.rs b/backend/server/src/handler/organisation.rs index 7950f1fb..d9c90566 100644 --- a/backend/server/src/handler/organisation.rs +++ b/backend/server/src/handler/organisation.rs @@ -1,9 +1,8 @@ -use crate::models; use crate::models::app::AppState; use crate::models::auth::SuperUser; use crate::models::auth::{AuthUser, OrganisationAdmin}; -use crate::models::campaign::Campaign; -use crate::models::email_template::EmailTemplate; +use crate::models::campaign::{Campaign, NewCampaign}; +use crate::models::email_template::{EmailTemplate, NewEmailTemplate}; use crate::models::error::ChaosError; use crate::models::organisation::{ AdminToRemove, AdminUpdateList, NewOrganisation, Organisation, SlugCheck, @@ -167,7 +166,7 @@ impl OrganisationHandler { Path(id): Path, State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_campaign( id, @@ -199,12 +198,13 @@ impl OrganisationHandler { Path(id): Path, State(state): State, _admin: OrganisationAdmin, - Json(request_body): Json, + Json(request_body): Json, ) -> Result { Organisation::create_email_template( id, request_body.name, - request_body.template, + request_body.template_subject, + request_body.template_body, &state.db, state.snowflake_generator, ) diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index c9f51772..793ef0db 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -35,7 +35,6 @@ pub struct Answer { #[derive(Deserialize)] pub struct NewAnswer { - pub application_id: i64, pub question_id: i64, #[serde(flatten)] @@ -320,7 +319,7 @@ impl Answer { id: i64, transaction: &mut Transaction<'_, Postgres>, ) -> Result<(), ChaosError> { - sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id) + let _ = sqlx::query!("DELETE FROM answers WHERE id = $1 RETURNING id", id) .fetch_one(transaction.deref_mut()) .await?; @@ -355,7 +354,7 @@ impl AnswerData { multi_option_answers: Option>, ranking_answers: Option>, ) -> Self { - return match question_type { + match question_type { QuestionType::ShortAnswer => { let answer = short_answer_answer.expect("Data should exist for ShortAnswer variant"); @@ -376,18 +375,18 @@ impl AnswerData { let options = ranking_answers.expect("Data should exist for Ranking variant"); AnswerData::Ranking(options) } - }; + } } pub fn validate(&self) -> Result<(), ChaosError> { match self { Self::ShortAnswer(text) => { - if text.len() == 0 { + if text.is_empty() { return Err(ChaosError::BadRequest); } } Self::MultiSelect(data) | Self::Ranking(data) => { - if data.len() == 0 { + if data.is_empty() { return Err(ChaosError::BadRequest); } } diff --git a/backend/server/src/models/app.rs b/backend/server/src/models/app.rs index 5fd59143..fd42647d 100644 --- a/backend/server/src/models/app.rs +++ b/backend/server/src/models/app.rs @@ -9,6 +9,7 @@ use crate::handler::question::QuestionHandler; use crate::handler::rating::RatingHandler; use crate::handler::role::RoleHandler; use crate::handler::user::UserHandler; +use crate::models::email::{ChaosEmail, EmailCredentials}; use crate::models::error::ChaosError; use crate::models::storage::Storage; use axum::routing::{get, patch, post}; @@ -31,6 +32,7 @@ pub struct AppState { pub jwt_validator: Validation, pub snowflake_generator: SnowflakeIdGenerator, pub storage_bucket: Bucket, + pub email_credentials: EmailCredentials, } pub async fn app() -> Result { @@ -65,6 +67,9 @@ pub async fn app() -> Result { // Initialise S3 bucket let storage_bucket = Storage::init_bucket(); + // Initialise email credentials + let email_credentials = ChaosEmail::setup_credentials(); + // Add all data to AppState let state = AppState { db: pool, @@ -75,6 +80,7 @@ pub async fn app() -> Result { jwt_validator, snowflake_generator, storage_bucket, + email_credentials, }; Ok(Router::new() @@ -242,6 +248,14 @@ pub async fn app() -> Result { "/api/v1/application/:application_id/answers/role/:role_id", get(AnswerHandler::get_all_by_application_and_role), ) + .route( + "/api/v1/application/:application_id/roles", + patch(ApplicationHandler::update_roles) + ) + .route( + "/api/v1/application/:application_id/submit", + post(ApplicationHandler::submit) + ) .route( "/api/v1/answer/:answer_id", patch(AnswerHandler::update).delete(AnswerHandler::delete), diff --git a/backend/server/src/models/application.rs b/backend/server/src/models/application.rs index 1cd9a74f..0dcafab1 100644 --- a/backend/server/src/models/application.rs +++ b/backend/server/src/models/application.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::models::error::ChaosError; use crate::models::user::UserDetails; use chrono::{DateTime, Utc}; @@ -5,6 +6,12 @@ use serde::{Deserialize, Serialize}; use snowflake::SnowflakeIdGenerator; use sqlx::{FromRow, Pool, Postgres, Transaction}; use std::ops::DerefMut; +use axum::{async_trait, RequestPartsExt}; +use axum::extract::{FromRef, FromRequestParts, Path}; +use axum::http::request::Parts; +use crate::models::app::AppState; +use crate::service::answer::assert_answer_application_is_open; +use crate::service::application::{assert_application_is_open}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct Application { @@ -27,6 +34,7 @@ pub struct ApplicationRole { pub id: i64, pub application_id: i64, pub campaign_role_id: i64, + pub preference: i32, } #[derive(Deserialize, Serialize)] @@ -64,6 +72,12 @@ pub struct ApplicationData { pub struct ApplicationAppliedRoleDetails { pub campaign_role_id: i64, pub role_name: String, + pub preference: i32, +} + +#[derive(Deserialize)] +pub struct ApplicationRoleUpdate { + pub roles: Vec, } #[derive(Deserialize, Serialize, sqlx::Type, Clone, Debug)] @@ -101,11 +115,12 @@ impl Application { for role_applied in application_data.applied_roles { sqlx::query!( " - INSERT INTO application_roles (application_id, campaign_role_id) - VALUES ($1, $2) + INSERT INTO application_roles (application_id, campaign_role_id, preference) + VALUES ($1, $2, $3) ", id, - role_applied.campaign_role_id + role_applied.campaign_role_id, + role_applied.preference ) .execute(transaction.deref_mut()) .await?; @@ -115,7 +130,7 @@ impl Application { } /* - Get Application given an application id + Get Application given an application id. Used by application viewers */ pub async fn get( id: i64, @@ -129,8 +144,10 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id - WHERE a.id = $1 + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN campaigns c ON c.id = a.campaign_id + WHERE a.id = $1 AND a.submitted = true ", id ) @@ -140,9 +157,10 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -171,7 +189,7 @@ impl Application { } /* - Get All applications that apply for a given role + Get All applications that apply for a given role. Used by application viewers */ pub async fn get_from_role_id( role_id: i64, @@ -185,8 +203,11 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id LEFT JOIN application_roles ar on ar.application_id = a.id - WHERE ar.id = $1 + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN application_roles ar on ar.application_id = a.id + JOIN campaigns c on c.id = a.campaign_id + WHERE ar.id = $1 AND a.submitted = true ", role_id ) @@ -198,9 +219,10 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -234,7 +256,7 @@ impl Application { } /* - Get All applications that apply for a given campaign + Get All applications that apply for a given campaign. Used by application viewers */ pub async fn get_from_campaign_id( campaign_id: i64, @@ -248,8 +270,10 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id - WHERE a.campaign_id = $1 + FROM applications a + JOIN users u ON u.id = a.user_id + JOIN campaigns c ON c.id = a.campaign_id + WHERE a.campaign_id = $1 AND a.submitted = true ", campaign_id ) @@ -261,9 +285,10 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -297,7 +322,7 @@ impl Application { } /* - Get All applications that are made by a given user + Get All applications that are made by a given user. Used by user */ pub async fn get_from_user_id( user_id: i64, @@ -311,7 +336,7 @@ impl Application { u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender, u.pronouns AS user_pronouns, u.degree_name AS user_degree_name, u.degree_starting_year AS user_degree_starting_year - FROM applications a LEFT JOIN users u ON u.id = a.user_id + FROM applications a JOIN users u ON u.id = a.user_id WHERE a.user_id = $1 ", user_id @@ -324,9 +349,10 @@ impl Application { let applied_roles = sqlx::query_as!( ApplicationAppliedRoleDetails, " - SELECT application_roles.campaign_role_id, campaign_roles.name AS role_name + SELECT application_roles.campaign_role_id, + application_roles.preference, campaign_roles.name AS role_name FROM application_roles - LEFT JOIN campaign_roles + JOIN campaign_roles ON application_roles.campaign_role_id = campaign_roles.id WHERE application_id = $1 ", @@ -338,8 +364,9 @@ impl Application { let details = ApplicationDetails { id: application_data.id, campaign_id: application_data.campaign_id, - status: application_data.status, - private_status: application_data.private_status, + status: application_data.status.clone(), + // To reuse struct, do not show use private status + private_status: application_data.status, applied_roles, user: UserDetails { id: application_data.user_id, @@ -398,4 +425,105 @@ impl Application { Ok(()) } + + pub async fn update_roles( + id: i64, + roles: Vec, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + sqlx::query!( + " + DELETE FROM application_roles WHERE application_id = $1 + ", + id + ) + .execute(transaction.deref_mut()) + .await?; + + // Insert into table application_roles + for role in roles { + sqlx::query!( + " + INSERT INTO application_roles (application_id, campaign_role_id, preference) + VALUES ($1, $2, $3) + ", + id, + role.campaign_role_id, + role.preference + ) + .execute(transaction.deref_mut()) + .await?; + } + + Ok(()) + } + + pub async fn submit( + id: i64, + transaction: &mut Transaction<'_, Postgres>, + ) -> Result<(), ChaosError> { + let _ = sqlx::query!( + " + UPDATE applications SET submitted = true WHERE id = $1 RETURNING id + ", + id + ) + .fetch_one(transaction.deref_mut()) + .await?; + + Ok(()) + } +} + + +pub struct OpenApplicationByApplicationId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByApplicationId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + let application_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_application_is_open(application_id, &app_state.db).await?; + + Ok(OpenApplicationByApplicationId) + } } + +pub struct OpenApplicationByAnswerId; + +#[async_trait] +impl FromRequestParts for OpenApplicationByAnswerId +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + let answer_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_answer_application_is_open(answer_id, &app_state.db).await?; + + Ok(OpenApplicationByAnswerId) + } +} \ No newline at end of file diff --git a/backend/server/src/models/campaign.rs b/backend/server/src/models/campaign.rs index 5897dc27..d8062327 100644 --- a/backend/server/src/models/campaign.rs +++ b/backend/server/src/models/campaign.rs @@ -1,11 +1,16 @@ +use std::collections::HashMap; use chrono::{DateTime, Utc}; use s3::Bucket; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Transaction}; use sqlx::{Pool, Postgres}; use std::ops::DerefMut; +use axum::{async_trait, RequestPartsExt}; +use axum::extract::{FromRef, FromRequestParts, Path}; +use axum::http::request::Parts; use uuid::Uuid; - +use crate::models::app::AppState; +use crate::service::campaign::assert_campaign_is_open; use super::{error::ChaosError, storage::Storage}; #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] @@ -36,6 +41,7 @@ pub struct CampaignDetails { pub starts_at: DateTime, pub ends_at: DateTime, } + #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct OrganisationCampaign { pub id: i64, @@ -47,6 +53,15 @@ pub struct OrganisationCampaign { pub ends_at: DateTime, } +#[derive(Deserialize)] +pub struct NewCampaign { + pub slug: String, + pub name: String, + pub description: Option, + pub starts_at: DateTime, + pub ends_at: DateTime +} + #[derive(Deserialize, Serialize, Clone, FromRow, Debug)] pub struct CampaignUpdate { pub slug: String, @@ -226,3 +241,29 @@ impl Campaign { Ok(()) } } + +pub struct OpenCampaign; + +#[async_trait] +impl FromRequestParts for OpenCampaign +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = ChaosError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + let campaign_id = *parts + .extract::>>() + .await + .map_err(|_| ChaosError::BadRequest)? + .get("application_id") + .ok_or(ChaosError::BadRequest)?; + + assert_campaign_is_open(campaign_id, &app_state.db).await?; + + Ok(OpenCampaign) + } +} diff --git a/backend/server/src/models/email.rs b/backend/server/src/models/email.rs new file mode 100644 index 00000000..e36a090d --- /dev/null +++ b/backend/server/src/models/email.rs @@ -0,0 +1,70 @@ +use crate::models::error::ChaosError; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; +use serde::Serialize; +use std::env; + +pub struct ChaosEmail; + +#[derive(Clone)] +pub struct EmailCredentials { + pub credentials: Credentials, + pub email_host: String, +} + +#[derive(Serialize)] +pub struct EmailParts { + pub subject: String, + pub body: String, +} + +impl ChaosEmail { + pub fn setup_credentials() -> EmailCredentials { + let smtp_username = env::var("SMTP_USERNAME") + .expect("Error getting SMTP USERNAME") + .to_string(); + + let smtp_password = env::var("SMTP_PASSWORD") + .expect("Error getting SMTP PASSWORD") + .to_string(); + + let email_host = env::var("SMTP_HOST") + .expect("Error getting SMTP HOST") + .to_string(); + + EmailCredentials { + credentials: Credentials::new(smtp_username, smtp_password), + email_host, + } + } + + fn new_connection( + credentials: EmailCredentials, + ) -> Result, ChaosError> { + Ok( + AsyncSmtpTransport::::relay(&credentials.email_host)? + .credentials(credentials.credentials) + .build(), + ) + } + + pub async fn send_message( + recipient_name: String, + recipient_email_address: String, + subject: String, + body: String, + credentials: EmailCredentials, + ) -> Result<(), ChaosError> { + let message = Message::builder() + .from("Chaos Subcommittee Recruitment ".parse()?) + .reply_to("help@chaos.devsoc.app".parse()?) + .to(format!("{recipient_name} <{recipient_email_address}>").parse()?) + .subject(subject) + .body(body)?; + + let mailer = Self::new_connection(credentials)?; + mailer.send(message).await?; + + Ok(()) + } +} diff --git a/backend/server/src/models/email_template.rs b/backend/server/src/models/email_template.rs index dd91cf00..c1628f1f 100644 --- a/backend/server/src/models/email_template.rs +++ b/backend/server/src/models/email_template.rs @@ -1,3 +1,4 @@ +use crate::models::email::EmailParts; use crate::models::error::ChaosError; use chrono::{DateTime, Local, Utc}; use handlebars::Handlebars; @@ -18,7 +19,15 @@ pub struct EmailTemplate { pub id: i64, pub organisation_id: i64, pub name: String, - pub template: String, + pub template_subject: String, + pub template_body: String, +} + +#[derive(Deserialize, Serialize)] +pub struct NewEmailTemplate { + pub name: String, + pub template_subject: String, + pub template_body: String, } impl EmailTemplate { @@ -55,16 +64,18 @@ impl EmailTemplate { pub async fn update( id: i64, name: String, - template: String, + template_subject: String, + template_body: String, pool: &Pool, ) -> Result<(), ChaosError> { let _ = sqlx::query!( " - UPDATE email_templates SET name = $2, template = $3 WHERE id = $1 RETURNING id + UPDATE email_templates SET name = $2, template_subject = $3, template_body = $4 WHERE id = $1 RETURNING id ", id, name, - template + template_subject, + template_body ) .fetch_one(pool) .await?; @@ -88,11 +99,12 @@ impl EmailTemplate { expiry_date: DateTime, email_template_id: i64, transaction: &mut Transaction<'_, Postgres>, - ) -> Result { + ) -> Result { let template = EmailTemplate::get(email_template_id, transaction).await?; let mut handlebars = Handlebars::new(); - handlebars.register_template_string("template", template.template)?; + handlebars.register_template_string("template_subject", template.template_subject)?; + handlebars.register_template_string("template_body", template.template_body)?; let mut data = HashMap::new(); data.insert("name", name); @@ -107,8 +119,9 @@ impl EmailTemplate { .to_string(), ); - let final_string = handlebars.render("template", &data)?; + let subject = handlebars.render("template_subject", &data)?; + let body = handlebars.render("template_body", &data)?; - Ok(final_string) + Ok(EmailParts { subject, body }) } } diff --git a/backend/server/src/models/error.rs b/backend/server/src/models/error.rs index 25f3f794..c666f96e 100644 --- a/backend/server/src/models/error.rs +++ b/backend/server/src/models/error.rs @@ -19,6 +19,12 @@ pub enum ChaosError { #[error("Bad request")] BadRequest, + #[error("Application closed")] + ApplicationClosed, + + #[error("Campagin closed")] + CampaignClosed, + #[error("SQLx error")] DatabaseError(#[from] sqlx::Error), @@ -45,6 +51,15 @@ pub enum ChaosError { #[error("Template rendering error")] TemplateRendorError(#[from] handlebars::RenderError), + + #[error("Lettre error")] + LettreError(#[from] lettre::error::Error), + + #[error("Email address error")] + AddressError(#[from] lettre::address::AddressError), + + #[error("SMTP transport error")] + SmtpTransportError(#[from] lettre::transport::smtp::Error), } /// Implementation for converting errors into responses. Manages error code and message returned. @@ -57,6 +72,8 @@ impl IntoResponse for ChaosError { (StatusCode::FORBIDDEN, "Forbidden operation").into_response() } ChaosError::BadRequest => (StatusCode::BAD_REQUEST, "Bad request").into_response(), + ChaosError::ApplicationClosed => (StatusCode::BAD_REQUEST, "Application closed").into_response(), + ChaosError::CampaignClosed => (StatusCode::BAD_REQUEST, "Campaign closed").into_response(), ChaosError::DatabaseError(db_error) => match db_error { // We only care about the RowNotFound error, as others are miscellaneous DB errors. sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index e2d03ca5..d1922411 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -3,6 +3,7 @@ pub mod app; pub mod application; pub mod auth; pub mod campaign; +pub mod email; pub mod email_template; pub mod error; pub mod offer; diff --git a/backend/server/src/models/offer.rs b/backend/server/src/models/offer.rs index d0201756..60fa94ee 100644 --- a/backend/server/src/models/offer.rs +++ b/backend/server/src/models/offer.rs @@ -1,3 +1,4 @@ +use crate::models::email::{ChaosEmail, EmailCredentials, EmailParts}; use crate::models::email_template::EmailTemplate; use crate::models::error::ChaosError; use chrono::{DateTime, Utc}; @@ -186,9 +187,9 @@ impl Offer { pub async fn preview_email( id: i64, transaction: &mut Transaction<'_, Postgres>, - ) -> Result { + ) -> Result { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email( + let email_parts = EmailTemplate::generate_email( offer.user_name, offer.role_name, offer.organisation_name, @@ -198,16 +199,18 @@ impl Offer { transaction, ) .await?; - Ok(email) + + Ok(email_parts) } pub async fn send_offer( id: i64, transaction: &mut Transaction<'_, Postgres>, + email_credentials: EmailCredentials, ) -> Result<(), ChaosError> { let offer = Offer::get(id, transaction).await?; - let email = EmailTemplate::generate_email( - offer.user_name, + let email_parts = EmailTemplate::generate_email( + offer.user_name.clone(), offer.role_name, offer.organisation_name, offer.campaign_name, @@ -217,7 +220,14 @@ impl Offer { ) .await?; - // TODO: Send email e.g. send_email(offer.user_email, email).await?; + ChaosEmail::send_message( + offer.user_name, + offer.user_email, + email_parts.subject, + email_parts.body, + email_credentials, + ) + .await?; Ok(()) } } diff --git a/backend/server/src/models/organisation.rs b/backend/server/src/models/organisation.rs index 94824c37..ae3e5229 100644 --- a/backend/server/src/models/organisation.rs +++ b/backend/server/src/models/organisation.rs @@ -194,7 +194,7 @@ impl Organisation { Member, " SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members - LEFT JOIN users on users.id = organisation_members.user_id + JOIN users on users.id = organisation_members.user_id WHERE organisation_members.organisation_id = $1 AND organisation_members.role = $2 ", organisation_id, @@ -216,7 +216,7 @@ impl Organisation { Member, " SELECT organisation_members.user_id as id, organisation_members.role AS \"role: OrganisationRole\", users.name from organisation_members - LEFT JOIN users on users.id = organisation_members.user_id + JOIN users on users.id = organisation_members.user_id WHERE organisation_members.organisation_id = $1 ", organisation_id @@ -439,7 +439,8 @@ impl Organisation { pub async fn create_email_template( organisation_id: i64, name: String, - template: String, + template_subject: String, + template_body: String, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator, ) -> Result { @@ -447,13 +448,14 @@ impl Organisation { let _ = sqlx::query!( " - INSERT INTO email_templates (id, organisation_id, name, template) - VALUES ($1, $2, $3, $4) + INSERT INTO email_templates (id, organisation_id, name, template_subject, template_body) + VALUES ($1, $2, $3, $4, $5) ", id, organisation_id, name, - template + template_subject, + template_body ) .execute(pool) .await?; diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index db1f70f3..86957454 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -491,20 +491,11 @@ impl QuestionType { } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Default)] pub struct MultiOptionData { options: Vec, } -impl Default for MultiOptionData { - fn default() -> Self { - Self { - // Return an empty vector to be replaced by real data later on. - options: vec![], - } - } -} - /// Each of these structs represent a row in the `multi_option_question_options` /// table. For a `MultiChoice` question like "What is your favourite programming /// language?", there would be rows for "Rust", "Java" and "TypeScript". @@ -530,7 +521,7 @@ impl QuestionData { question_type: QuestionType, multi_option_data: Option>>, ) -> Self { - return if question_type == QuestionType::ShortAnswer { + if question_type == QuestionType::ShortAnswer { QuestionData::ShortAnswer } else if question_type == QuestionType::MultiChoice || question_type == QuestionType::MultiSelect @@ -551,7 +542,7 @@ impl QuestionData { } } else { QuestionData::ShortAnswer // Should never be reached, hence return ShortAnswer - }; + } } pub fn validate(&self) -> Result<(), ChaosError> { @@ -561,7 +552,7 @@ impl QuestionData { | Self::MultiSelect(data) | Self::DropDown(data) | Self::Ranking(data) => { - if data.options.len() > 0 { + if !data.options.is_empty() { return Ok(()); }; diff --git a/backend/server/src/models/storage.rs b/backend/server/src/models/storage.rs index d44b35aa..f547d0a9 100644 --- a/backend/server/src/models/storage.rs +++ b/backend/server/src/models/storage.rs @@ -37,7 +37,7 @@ impl Storage { endpoint, }; - let bucket = Bucket::new(&*bucket_name, region, credentials).unwrap(); + let bucket = Bucket::new(&bucket_name, region, credentials).unwrap(); // TODO: Change depending on style used by provider // bucket.set_path_style(); diff --git a/backend/server/src/service/answer.rs b/backend/server/src/service/answer.rs index 682656a0..a0f8ca5b 100644 --- a/backend/server/src/service/answer.rs +++ b/backend/server/src/service/answer.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -30,3 +31,27 @@ pub async fn user_is_answer_owner( Ok(()) } + +pub async fn assert_answer_application_is_open( + answer_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT app.submitted, c.ends_at FROM answers ans + JOIN applications app ON app.id = ans.application_id + JOIN campaigns c on c.id = app.campaign_id + WHERE ans.id = $1 + ", + answer_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +} \ No newline at end of file diff --git a/backend/server/src/service/application.rs b/backend/server/src/service/application.rs index dcdf755b..9198f167 100644 --- a/backend/server/src/service/application.rs +++ b/backend/server/src/service/application.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -60,3 +61,26 @@ pub async fn user_is_application_owner( Ok(()) } + +pub async fn assert_application_is_open( + application_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let application = sqlx::query!( + " + SELECT submitted, c.ends_at FROM applications a + JOIN campaigns c on c.id = a.campaign_id + WHERE a.id = $1 + ", + application_id + ) + .fetch_one(pool) + .await?; + + if application.submitted || application.ends_at <= time { + return Err(ChaosError::ApplicationClosed) + } + + Ok(()) +} diff --git a/backend/server/src/service/campaign.rs b/backend/server/src/service/campaign.rs index b9551473..fe041f96 100644 --- a/backend/server/src/service/campaign.rs +++ b/backend/server/src/service/campaign.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::models::error::ChaosError; use sqlx::{Pool, Postgres}; @@ -28,3 +29,24 @@ pub async fn user_is_campaign_admin( Ok(()) } + +pub async fn assert_campaign_is_open( + campaign_id: i64, + pool: &Pool, +) -> Result<(), ChaosError> { + let time = Utc::now(); + let campaign = sqlx::query!( + " + SELECT ends_at FROM campaigns WHERE id = $1 + ", + campaign_id + ) + .fetch_one(pool) + .await?; + + if campaign.ends_at <= time { + return Err(ChaosError::CampaignClosed) + } + + Ok(()) +} \ No newline at end of file diff --git a/backend/setup-dev-env.sh b/backend/setup-dev-env.sh index 0eaa03a0..f0f22268 100755 --- a/backend/setup-dev-env.sh +++ b/backend/setup-dev-env.sh @@ -19,6 +19,9 @@ S3_ACCESS_KEY="test_access_key" S3_SECRET_KEY="test_secret_key" S3_ENDPOINT="https://chaos-storage.s3.ap-southeast-1.amazonaws.com" S3_REGION_NAME="ap-southeast-1" +SMTP_USERNAME="test_username" +SMTP_PASSWORD="test_password" +SMTP_HOST="smtp.example.com" EOF # Check the user has all required tools installed.