diff --git a/common/openapi.yaml b/common/openapi.yaml new file mode 100644 index 0000000000..0793b6fb06 --- /dev/null +++ b/common/openapi.yaml @@ -0,0 +1,461 @@ +openapi: 3.1.0 +info: + title: p5.js Web Editor - 1.11.10 + description: |- + This is the api for the p5.js Web Editor + contact: + email: hello@p5js.org + license: + name: GNU LESSER GENERAL PUBLIC LICENSE 2.0 + url: https://github.com/processing/p5.js/blob/main/license.txt + version: 1.11.10 +externalDocs: + description: Find out more about the p5.js library + url: https://p5js.org/ +servers: + - url: https://editor.p5js.org + description: Production server + - url: http://localhost:8000 + description: Location development server +tags: + - name: Auth + description: Signup, login, verification, and password reset endpoints + - name: Account + description: Authenticated user account and API key management + - name: Preferences + description: User preference and cookie consent endpoints +paths: + /editor/signup: + post: + summary: Create a new user + operationId: createUser + tags: [Auth] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequestBody' + responses: + '200': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUser' + '422': + $ref: '#/components/responses/ValidationError' + '500': + $ref: '#/components/responses/ServerError' + + /editor/signup/duplicate_check: + get: + summary: Check for duplicate email or username + operationId: duplicateUserCheck + tags: [Auth] + parameters: + - name: check_type + in: query + required: true + schema: + type: string + enum: [email, username] + - name: email + in: query + schema: + type: string + - name: username + in: query + schema: + type: string + responses: + '200': + description: Returns whether the email or username exists + content: + application/json: + schema: + type: object + properties: + exists: + type: boolean + message: + type: string + type: + type: string + + /editor/verify/send: + post: + summary: Send email verification link + operationId: emailVerificationInitiate + tags: [Auth] + security: + - cookieAuth: [] + responses: + '200': + description: Verification email sent + content: + application/json: + schema: + $ref: '#/components/schemas/PublicUser' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: Email already verified + '500': + $ref: '#/components/responses/ServerError' + + /editor/verify: + get: + summary: Verify email using token + operationId: verifyEmail + tags: [Auth] + parameters: + - name: t + in: query + required: true + schema: + type: string + responses: + '200': + description: Email verified successfully + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + '401': + $ref: '#/components/responses/Unauthorized' + + /editor/account/api-keys: + post: + summary: Create API key + operationId: createApiKey + tags: [Account] + security: + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKeyRequestBody' + responses: + '200': + description: API key created + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeysResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/ServerError' + + /editor/account/api-keys/{keyId}: + delete: + summary: Remove API key + operationId: removeApiKey + tags: [Account] + security: + - cookieAuth: [] + parameters: + - name: keyId + in: path + required: true + schema: + type: string + responses: + '200': + description: API key removed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeysResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/ServerError' + + /editor/reset-password: + post: + summary: Initiate password reset + operationId: resetPasswordInitiate + tags: [Auth] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ResetPasswordInitiateRequestBody' + responses: + '200': + description: Reset email sent (even if user not found) + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponseBody' + '500': + $ref: '#/components/responses/ServerError' + + /editor/reset-password/{token}: + get: + summary: Validate reset password token + operationId: validateResetPasswordToken + tags: [Auth] + parameters: + - name: token + in: path + required: true + schema: + type: string + responses: + '200': + description: Token valid + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponseBody' + '401': + $ref: '#/components/responses/Unauthorized' + post: + summary: Update password using token + operationId: updatePassword + tags: [Auth] + parameters: + - name: token + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePasswordRequestBody' + responses: + '200': + description: Password updated successfully + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /editor/account: + put: + summary: Update account settings + operationId: updateSettings + tags: [Account] + security: + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateSettingsRequestBody' + responses: + '200': + description: Account updated successfully + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/ServerError' + + /editor/auth/github: + delete: + summary: Unlink GitHub account + operationId: unlinkGithub + tags: [Auth] + responses: + '200': + description: GitHub account unlinked + '404': + $ref: '#/components/responses/Unauthorized' + + /editor/auth/google: + delete: + summary: Unlink Google account + operationId: unlinkGoogle + tags: [Auth] + responses: + '200': + description: Google account unlinked + '404': + $ref: '#/components/responses/Unauthorized' + + /editor/cookie-consent: + put: + summary: Update cookie consent + operationId: updateCookieConsent + tags: [Preferences] + security: + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateCookieConsentRequestBody' + responses: + '200': + description: Cookie consent updated + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/ServerError' + + /editor/preferences: + put: + summary: Update user preferences + operationId: updatePreferences + tags: [Preferences] + security: + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePreferencesRequestBody' + responses: + '200': + description: Preferences updated + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/ServerError' + +components: + # TODO: not sure how passport works here + # securitySchemes: + # cookieAuth: + # type: apiKey + # in: cookie + # name: sessionId + + responses: + Unauthorized: + description: Unauthorized or invalid token + NotFound: + description: Resource not found + BadRequest: + description: Bad request + ValidationError: + description: Validation failed (e.g., duplicate email or username) + ServerError: + description: Internal server error + + schemas: + PublicUser: + type: object + properties: + id: + type: string + username: + type: string + email: + type: string + verified: + type: string + preferences: + type: object + nullable: true + + CreateUserRequestBody: + type: object + required: [username, email, password] + properties: + username: + type: string + email: + type: string + format: email + password: + type: string + format: password + + CreateApiKeyRequestBody: + type: object + required: [label] + properties: + label: + type: string + example: 'My Integration Key' + + ApiKeysResponse: + type: object + properties: + apiKeys: + type: array + items: + type: object + properties: + label: + type: string + token: + type: string + nullable: true + id: + type: string + + ResetPasswordInitiateRequestBody: + type: object + required: [email] + properties: + email: + type: string + format: email + + UpdatePasswordRequestBody: + type: object + required: [password] + properties: + password: + type: string + format: password + + UpdateSettingsRequestBody: + type: object + properties: + username: + type: string + email: + type: string + format: email + currentPassword: + type: string + format: password + newPassword: + type: string + format: password + + UpdateCookieConsentRequestBody: + type: object + required: [cookieConsent] + properties: + cookieConsent: + type: boolean + + UpdatePreferencesRequestBody: + type: object + required: [preferences] + properties: + preferences: + type: object + example: + appTheme: 'dark' + + GenericResponseBody: + type: object + properties: + success: + type: boolean + message: + type: string diff --git a/package-lock.json b/package-lock.json index 9edffa05e9..b1145d2332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,11 +115,13 @@ "stacktrace-js": "^2.0.2", "styled-components": "^5.3.0", "styled-theming": "^2.2.0", + "swagger-ui-express": "^5.0.1", "url": "^0.11.0", "uuid": "^8.3.2", "webpack": "^5.94.0", "webpack-dev-middleware": "^5.3.4", - "xhr": "^2.6.0" + "xhr": "^2.6.0", + "yamljs": "^0.3.0" }, "devDependencies": { "@babel/eslint-parser": "^7.17.0", @@ -11622,6 +11624,13 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -38619,6 +38628,30 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "node_modules/swagger-ui-dist": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", + "integrity": "sha512-BoiDSeT9PCtHfDYMgX5UpB/qTQy44UoSFRmzHqvhGfgxzEVPHxIW78a+HMLUIHnSFu3z63wjtbq6L6+Rto20Rw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/swc-loader": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", @@ -40682,6 +40715,20 @@ "node": ">= 6" } }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -49379,6 +49426,11 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==" + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -69525,6 +69577,22 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "swagger-ui-dist": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", + "integrity": "sha512-BoiDSeT9PCtHfDYMgX5UpB/qTQy44UoSFRmzHqvhGfgxzEVPHxIW78a+HMLUIHnSFu3z63wjtbq6L6+Rto20Rw==", + "requires": { + "@scarf/scarf": "=1.4.0" + } + }, + "swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "requires": { + "swagger-ui-dist": ">=5.0.0" + } + }, "swc-loader": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", @@ -71057,6 +71125,15 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 643d468aa3..0669a1267b 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,6 @@ "@types/nodemailer": "^7.0.1", "@types/nodemailer-mailgun-transport": "^1.4.6", "@types/passport": "^1.0.17", - "@types/passport": "^1.0.17", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", "@types/react-router-dom": "^5.3.3", @@ -306,10 +305,12 @@ "stacktrace-js": "^2.0.2", "styled-components": "^5.3.0", "styled-theming": "^2.2.0", + "swagger-ui-express": "^5.0.1", "url": "^0.11.0", "uuid": "^8.3.2", "webpack": "^5.94.0", "webpack-dev-middleware": "^5.3.4", - "xhr": "^2.6.0" + "xhr": "^2.6.0", + "yamljs": "^0.3.0" } } diff --git a/server/server.js b/server/server.js index 406ac78c35..bf5a5fe7ff 100644 --- a/server/server.js +++ b/server/server.js @@ -9,10 +9,15 @@ import passport from 'passport'; import path from 'path'; import basicAuth from 'express-basic-auth'; +// Swagger Open Api requirements: +import swaggerUi from 'swagger-ui-express'; +import YAML from 'yamljs'; + // Webpack Requirements import webpack from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from '@gatsbyjs/webpack-hot-middleware'; + import config from '../webpack/config.dev'; // Import all required modules @@ -32,6 +37,7 @@ import { renderIndex } from './views/index'; import { get404Sketch } from './views/404Page'; const app = new Express(); +const swaggerDocument = YAML.load('./common/openapi.yaml'); app.get('/health', (req, res) => res.json({ success: true })); @@ -55,6 +61,9 @@ if (process.env.NODE_ENV === 'development') { }) ); app.use(webpackHotMiddleware(compiler, { log: false })); + + // Show swagger openapi docs in local development only? + app.use('/open-api', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); } const mongoConnectionString = process.env.MONGO_URL;