From f5a6b1c37a59701fc793a249ff409014b15fbd0d Mon Sep 17 00:00:00 2001 From: Ahmed Al-Taiar Date: Tue, 31 Oct 2023 23:25:39 -0400 Subject: [PATCH] Role based access, and lots of style changes, login/signup pages still look funky in dark mode --- .env.defaults | 2 + .env.example | 2 + .../migration.sql | 33 ++++++++++++ api/db/schema.prisma | 17 ++---- api/src/functions/auth.ts | 10 +++- api/src/lib/auth.ts | 43 +++++++-------- api/src/services/parts/parts.scenarios.ts | 1 + api/src/services/parts/parts.ts | 23 ++++---- web/src/Routes.tsx | 2 +- web/src/components/Part/NewPart/NewPart.tsx | 4 +- web/src/components/Part/Part/Part.tsx | 4 +- web/src/components/Part/PartForm/PartForm.tsx | 53 ++++++------------- web/src/components/Part/Parts/Parts.tsx | 5 +- .../components/Part/PartsCell/PartsCell.tsx | 4 +- .../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 8 ++- .../ForgotPasswordPage/ForgotPasswordPage.tsx | 30 +++++------ web/src/pages/LoginPage/LoginPage.tsx | 33 ++++-------- .../ResetPasswordPage/ResetPasswordPage.tsx | 31 ++++------- web/src/pages/SignupPage/SignupPage.tsx | 52 +++++------------- web/src/scaffold.css | 50 ++++++----------- 20 files changed, 172 insertions(+), 235 deletions(-) create mode 100644 api/db/migrations/20231101015820_simplify_roles/migration.sql diff --git a/.env.defaults b/.env.defaults index 358c897..8e2a36c 100644 --- a/.env.defaults +++ b/.env.defaults @@ -19,3 +19,5 @@ PRISMA_HIDE_UPDATE_MESSAGE=true # LOG_LEVEL=debug REDWOOD_ENV_FILESTACK_API_KEY= REDWOOD_ENV_FILESTACK_SECRET= +SESSION_SECRET= +ADMIN_EMAILS= diff --git a/.env.example b/.env.example index d93c75b..b0e7081 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,5 @@ # LOG_LEVEL=trace REDWOOD_ENV_FILESTACK_API_KEY= REDWOOD_ENV_FILESTACK_SECRET= +SESSION_SECRET= +ADMIN_EMAILS=foo@bar.com,fizz@buzz.com,john@example.com diff --git a/api/db/migrations/20231101015820_simplify_roles/migration.sql b/api/db/migrations/20231101015820_simplify_roles/migration.sql new file mode 100644 index 0000000..0aa7f8b --- /dev/null +++ b/api/db/migrations/20231101015820_simplify_roles/migration.sql @@ -0,0 +1,33 @@ +/* + Warnings: + + - You are about to drop the `UserRole` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropIndex +DROP INDEX "UserRole_name_userId_key"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "UserRole"; +PRAGMA foreign_keys=on; + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "hashedPassword" TEXT NOT NULL, + "salt" TEXT NOT NULL, + "resetToken" TEXT, + "resetTokenExpiresAt" DATETIME, + "roles" TEXT NOT NULL DEFAULT 'user' +); +INSERT INTO "new_User" ("email", "firstName", "hashedPassword", "id", "lastName", "resetToken", "resetTokenExpiresAt", "salt") SELECT "email", "firstName", "hashedPassword", "id", "lastName", "resetToken", "resetTokenExpiresAt", "salt" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 3c226f5..8366d8c 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -18,24 +18,13 @@ model Part { } model User { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) firstName String lastName String - email String @unique + email String @unique hashedPassword String salt String resetToken String? resetTokenExpiresAt DateTime? - userRoles UserRole[] -} - -model UserRole { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) - name String - user User? @relation(fields: [userId], references: [id]) - userId Int? - - @@unique([name, userId]) + roles String @default("user") } diff --git a/api/src/functions/auth.ts b/api/src/functions/auth.ts index edf0041..fcad879 100644 --- a/api/src/functions/auth.ts +++ b/api/src/functions/auth.ts @@ -113,13 +113,21 @@ export const handler = async ( // If this returns anything else, it will be returned by the // `signUp()` function in the form of: `{ message: 'String here' }`. handler: ({ username, hashedPassword, salt, userAttributes }) => { + const adminEmails: string[] = process.env.ADMIN_EMAILS.split(',') + + let role = 'user' + const email = username.toLowerCase() + + if (adminEmails.includes(email)) role = 'admin' + return db.user.create({ data: { - email: username, + email: email, hashedPassword: hashedPassword, salt: salt, firstName: userAttributes.firstName, lastName: userAttributes.lastName, + roles: role, }, }) }, diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index bd808d0..7d12c33 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -27,7 +27,7 @@ export const getCurrentUser = async (session: Decoded) => { return await db.user.findUnique({ where: { id: session.id }, - select: { id: true, firstName: true }, + select: { id: true, firstName: true, roles: true }, }) } @@ -59,32 +59,27 @@ export const hasRole = (roles: AllowedRoles): boolean => { return false } - const currentUserRoles = context.currentUser?.roles - - if (typeof roles === 'string') { - if (typeof currentUserRoles === 'string') { - // roles to check is a string, currentUser.roles is a string - return currentUserRoles === roles - } else if (Array.isArray(currentUserRoles)) { - // roles to check is a string, currentUser.roles is an array - return currentUserRoles?.some((allowedRole) => roles === allowedRole) + // If your User model includes roles, uncomment the role checks on currentUser + if (roles) { + if (Array.isArray(roles)) { + // the line below has changed + if (context.currentUser.roles) + return context.currentUser.roles + .split(',') + .some((role) => roles.includes(role)) } + + if (typeof roles === 'string') { + // the line below has changed + if (context.currentUser.roles) + return context.currentUser.roles.split(',').includes(roles) + } + + // roles not found + return false } - if (Array.isArray(roles)) { - if (Array.isArray(currentUserRoles)) { - // roles to check is an array, currentUser.roles is an array - return currentUserRoles?.some((allowedRole) => - roles.includes(allowedRole) - ) - } else if (typeof currentUserRoles === 'string') { - // roles to check is an array, currentUser.roles is a string - return roles.some((allowedRole) => currentUserRoles === allowedRole) - } - } - - // roles not found - return false + return true } /** diff --git a/api/src/services/parts/parts.scenarios.ts b/api/src/services/parts/parts.scenarios.ts index 81e0d58..fc75ff5 100644 --- a/api/src/services/parts/parts.scenarios.ts +++ b/api/src/services/parts/parts.scenarios.ts @@ -1,4 +1,5 @@ import type { Prisma, Part } from '@prisma/client' + import type { ScenarioData } from '@redwoodjs/testing/api' export const standard = defineScenario({ diff --git a/api/src/services/parts/parts.ts b/api/src/services/parts/parts.ts index 7eae11a..63b31e6 100644 --- a/api/src/services/parts/parts.ts +++ b/api/src/services/parts/parts.ts @@ -29,18 +29,21 @@ export const updatePart: MutationResolvers['updatePart'] = ({ id, input }) => { export const deletePart: MutationResolvers['deletePart'] = async ({ id }) => { const client = Filestack.init(process.env.REDWOOD_ENV_FILESTACK_API_KEY) const part = await db.part.findUnique({ where: { id } }) - const handle = part.imageUrl.split('/').pop() - const security = Filestack.getSecurity( - { - expiry: new Date().getTime() + 5 * 60 * 1000, - handle, - call: ['remove'], - }, - process.env.REDWOOD_ENV_FILESTACK_SECRET - ) + if (!part.imageUrl.includes('no_image.png')) { + const handle = part.imageUrl.split('/').pop() - await client.remove(handle, security) + const security = Filestack.getSecurity( + { + expiry: new Date().getTime() + 5 * 60 * 1000, + handle, + call: ['remove'], + }, + process.env.REDWOOD_ENV_FILESTACK_SECRET + ) + + await client.remove(handle, security) + } return db.part.delete({ where: { id }, diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index 42cdee9..4268f80 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -21,7 +21,7 @@ const Routes = () => { - + diff --git a/web/src/components/Part/NewPart/NewPart.tsx b/web/src/components/Part/NewPart/NewPart.tsx index e548441..9f37781 100644 --- a/web/src/components/Part/NewPart/NewPart.tsx +++ b/web/src/components/Part/NewPart/NewPart.tsx @@ -1,11 +1,11 @@ +import type { CreatePartInput } from 'types/graphql' + import { navigate, routes } from '@redwoodjs/router' import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import PartForm from 'src/components/Part/PartForm' -import type { CreatePartInput } from 'types/graphql' - const CREATE_PART_MUTATION = gql` mutation CreatePartMutation($input: CreatePartInput!) { createPart(input: $input) { diff --git a/web/src/components/Part/Part/Part.tsx b/web/src/components/Part/Part/Part.tsx index 5a72a45..09aba8c 100644 --- a/web/src/components/Part/Part/Part.tsx +++ b/web/src/components/Part/Part/Part.tsx @@ -75,13 +75,13 @@ const Part = ({ part }: Props) => {