diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..1bed5bce Binary files /dev/null and b/.DS_Store differ diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 00000000..a0862f82 Binary files /dev/null and b/backend/.DS_Store differ diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json index ef3c4735..43d0c7c3 100644 --- a/backend/.eslintrc.json +++ b/backend/.eslintrc.json @@ -39,6 +39,9 @@ // "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", // Avoid bugs. "@typescript-eslint/no-shadow": ["error", { "ignoreTypeValueShadow": true }], diff --git a/backend/.gitignore b/backend/.gitignore index 552d1bbb..cbba7d16 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,6 @@ +# Firebase Service Account Key +src/firebase/ServiceAccountKey.json + .env node_modules/ .eslintcache diff --git a/backend/package-lock.json b/backend/package-lock.json index 7ade0398..e41ece62 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2945,9 +2945,9 @@ } }, "node_modules/google-gax": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.0.5.tgz", - "integrity": "sha512-yLoYtp4zE+8OQA74oBEbNkbzI6c95W01JSL7RqC8XERKpRvj3ytZp1dgnbA6G9aRsc8pZB25xWYBcCmrbYOEhA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.1.0.tgz", + "integrity": "sha512-VP5MYsIXEoXmdeHZl1Qsjv89PvE+LT8fw/2jxpFQtFed22YYAHgiTUuMfj2RWlGJUmRaYEMxBRBDWj+q/hOGQg==", "optional": true, "dependencies": { "@grpc/grpc-js": "~1.9.6", diff --git a/backend/src/app.ts b/backend/src/app.ts index fbd9a397..44a06dce 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -6,6 +6,7 @@ import studentRoutes from "../src/routes/student"; import { mongoURI, port } from "./config"; import { errorHandler } from "./errors/handler"; +import { userRouter } from "./routes/user"; /** * Express server application class @@ -17,6 +18,7 @@ class Server { // initialize server app const server = new Server(); +// Connect to MongoDB void mongoose .connect(mongoURI) .then(() => { @@ -26,9 +28,13 @@ void mongoose console.log(error); }); +// Middleware server.app.use(json()); +// Routes +server.app.use("/user", userRouter); server.app.use("/student", studentRoutes); +// Error Handler server.app.use(errorHandler); // make server listen on some port diff --git a/backend/src/controllers/student.ts b/backend/src/controllers/student.ts index ca0eda6e..f077b052 100644 --- a/backend/src/controllers/student.ts +++ b/backend/src/controllers/student.ts @@ -6,8 +6,8 @@ import { RequestHandler } from "express"; import { validationResult } from "express-validator"; -import { ValidationError } from "../errors/validation"; import StudentModel from "../models/student"; +import validationErrorParser from "../util/validationErrorParser"; export type contact = { lastName: string; @@ -32,17 +32,10 @@ export type typedModel = { }; export const createStudent: RequestHandler = async (req, res, next) => { - const errors = validationResult(req); - try { - if (!errors.isEmpty()) { - let errorString = ""; - - for (const error of errors.array()) { - errorString += error.msg + " "; - } - throw new ValidationError(errorString); - } + const errors = validationResult(req); + + validationErrorParser(errors); const newStudent = await StudentModel.create(req.body as typedModel); diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts new file mode 100644 index 00000000..70c8e80e --- /dev/null +++ b/backend/src/controllers/user.ts @@ -0,0 +1,53 @@ +import { NextFunction, Request, Response } from "express"; +import { validationResult } from "express-validator"; +import admin from "firebase-admin"; + +import UserModel from "../models/user"; +import { firebaseAuth } from "../util/firebase"; +import validationErrorParser from "../util/validationErrorParser"; + +// Define the type for req.body +type CreateUserRequestBody = { + name: string; + accountType: "admin" | "team"; + email: string; + password: string; +}; + +export const createUser = async ( + req: Request, Record, CreateUserRequestBody>, + res: Response, + nxt: NextFunction, +) => { + try { + // Check for validation errors + const errors = validationResult(req); + + validationErrorParser(errors); + + const { name, accountType, email, password } = req.body; + + // Create user in Firebase + const userRecord = await firebaseAuth.createUser({ + email, + password, + } as admin.auth.CreateRequest); // Type assertion + + // Set custom claim for accountType (“admin” | “team”) + await firebaseAuth.setCustomUserClaims(userRecord.uid, { accountType }); + + const newUser = await UserModel.create({ + _id: userRecord.uid, // Set document id to firebaseUID (Linkage between Firebase and MongoDB) + name, + accountType, + // approvalStatus default false in User constructor + }); + + res.status(201).json(newUser); + } catch (error) { + console.error(error); + nxt(error); + } + + return; +}; diff --git a/backend/src/errors/index.ts b/backend/src/errors/index.ts index 0855cdc4..5608e4e0 100644 --- a/backend/src/errors/index.ts +++ b/backend/src/errors/index.ts @@ -1,2 +1,3 @@ export * from "./errors"; export * from "./internal"; +export * from "./validation"; diff --git a/backend/src/errors/internal.ts b/backend/src/errors/internal.ts index f46d33ee..dceb4454 100644 --- a/backend/src/errors/internal.ts +++ b/backend/src/errors/internal.ts @@ -8,5 +8,6 @@ export class InternalError extends CustomError { static NO_APP_PORT = new InternalError(0, 500, NO_APP_PORT); static NO_MONGO_URI = new InternalError(1, 500, NO_MONGO_URI); + static NO_SERVICE_ACCOUNT_KEY = new InternalError(5, 500, NO_SERVICE_ACCOUNT_KEY); } diff --git a/backend/src/errors/validation.ts b/backend/src/errors/validation.ts index 18567f7e..608fa7c5 100644 --- a/backend/src/errors/validation.ts +++ b/backend/src/errors/validation.ts @@ -1,7 +1,10 @@ -import { CustomError } from "../errors"; +import { CustomError } from "./errors"; + +const USER_CREATION_UNSUCCESSFUL = "User not created successfully"; export class ValidationError extends CustomError { - constructor(message: string) { - super(0, 400, "VALIDATION ERROR: " + message); + constructor(code: number, status: number, message: string) { + super(code, status, "VALIDATION ERROR: " + message); } + static USER_CREATION_UNSUCCESSFUL = new ValidationError(1, 400, USER_CREATION_UNSUCCESSFUL); } diff --git a/backend/src/models/.keep b/backend/src/models/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts new file mode 100644 index 00000000..4c7a7cf1 --- /dev/null +++ b/backend/src/models/User.ts @@ -0,0 +1,18 @@ +import mongoose, { InferSchemaType } from "mongoose"; + +// export type UserDocument = { +// name: string; +// accountType: "admin" | "team"; // NOTE Also stored on Firebase using Custom Claims +// approvalStatus: boolean; +// }; + +const userSchema = new mongoose.Schema({ + _id: { type: String, required: true }, // Set _id to firebaseUid; Linkage between firebase account and user document on MongoDb + name: { type: String, required: true }, + accountType: { type: String, enum: ["admin", "team"], required: true }, + approvalStatus: { type: Boolean, default: false }, // default false +}); + +type User = InferSchemaType; + +export default mongoose.model("User", userSchema); diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts new file mode 100644 index 00000000..0546a63a --- /dev/null +++ b/backend/src/routes/user.ts @@ -0,0 +1,12 @@ +import express from "express"; + +import * as UserController from "../controllers/user"; +import * as UserValidator from "../validators/user"; + +const router = express.Router(); + +router.use(express.json()); + +router.post("/", UserValidator.createUser, UserController.createUser); + +export { router as userRouter }; diff --git a/backend/src/util/validationErrorParser.ts b/backend/src/util/validationErrorParser.ts new file mode 100644 index 00000000..48cb1f02 --- /dev/null +++ b/backend/src/util/validationErrorParser.ts @@ -0,0 +1,26 @@ +import { Result, ValidationError } from "express-validator"; + +import { ValidationError as InputError } from "../errors/validation"; + +/** + * Parses through errors thrown by validator (if any exist). Error messages are + * added to a string and that string is used as the error message for the HTTP + * error. + * + * @param errors the validation result provided by express validator middleware + */ +const validationErrorParser = (errors: Result) => { + if (!errors.isEmpty()) { + let errorString = ""; + + // parse through errors returned by the validator and append them to the error string + for (const error of errors.array()) { + errorString += error.msg + " "; + } + + // trim removes the trailing space created in the for loop + throw new InputError(0, 400, errorString); + } +}; + +export default validationErrorParser; diff --git a/backend/src/validators/user.ts b/backend/src/validators/user.ts new file mode 100644 index 00000000..50fd79c1 --- /dev/null +++ b/backend/src/validators/user.ts @@ -0,0 +1,8 @@ +import { ValidationChain, body } from "express-validator"; + +export const createUser: ValidationChain[] = [ + body("name").notEmpty().isString(), + body("accountType").notEmpty().isIn(["admin", "team"]), + body("email").notEmpty().isEmail(), + body("password").notEmpty().isString().isLength({ min: 6 }), +]; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index bb2f7f66..b1b1546d 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -39,7 +39,7 @@ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true /* Enable importing .json files. */, // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */