1
0

Role based access, and lots of style changes, login/signup pages still look funky in dark mode

This commit is contained in:
Ahmed Al-Taiar
2023-10-31 23:25:39 -04:00
parent fcdacd844f
commit f5a6b1c37a
20 changed files with 172 additions and 235 deletions

View File

@ -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=

View File

@ -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

View File

@ -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;

View File

@ -26,16 +26,5 @@ model User {
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")
}

View File

@ -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,
},
})
},

View File

@ -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,34 +59,29 @@ export const hasRole = (roles: AllowedRoles): boolean => {
return false
}
const currentUserRoles = context.currentUser?.roles
// 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') {
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 (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)
}
// the line below has changed
if (context.currentUser.roles)
return context.currentUser.roles.split(',').includes(roles)
}
// roles not found
return false
}
return true
}
/**
* Use requireAuth in your services to check that a user is logged in,
* whether or not they are assigned a role, and optionally raise an

View File

@ -1,4 +1,5 @@
import type { Prisma, Part } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.PartCreateArgs>({

View File

@ -29,6 +29,8 @@ 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 } })
if (!part.imageUrl.includes('no_image.png')) {
const handle = part.imageUrl.split('/').pop()
const security = Filestack.getSecurity(
@ -41,6 +43,7 @@ export const deletePart: MutationResolvers['deletePart'] = async ({ id }) => {
)
await client.remove(handle, security)
}
return db.part.delete({
where: { id },

View File

@ -21,7 +21,7 @@ const Routes = () => {
<Route path="/signup" page={SignupPage} name="signup" />
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
<Private unauthenticated="home">
<Private unauthenticated="home" roles="admin">
<Set wrap={ScaffoldLayout} title="Parts" titleTo="parts" buttonLabel="New Part" buttonTo="newPart">
<Route path="/admin/parts/new" page={PartNewPartPage} name="newPart" />
<Route path="/admin/parts/{id:Int}/edit" page={PartEditPartPage} name="editPart" />

View File

@ -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) {

View File

@ -75,13 +75,13 @@ const Part = ({ part }: Props) => {
<nav className="rw-button-group">
<Link
to={routes.editPart({ id: part.id })}
className="rw-button rw-button-blue"
className="rw-button btn-primary"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
className="rw-button btn-error"
onClick={() => onDeleteClick(part.id)}
>
Delete

View File

@ -11,6 +11,7 @@ import {
TextField,
NumberField,
Submit,
TextAreaField,
} from '@redwoodjs/forms'
import type { RWGqlError } from '@redwoodjs/forms'
@ -36,6 +37,7 @@ const PartForm = (props: PartFormProps) => {
}
const preview = (url: string) => {
if (url.includes('no_image.png')) return url
const parts = url.split('/')
parts.splice(3, 0, 'resize=height:500')
return parts.join('/')
@ -51,58 +53,37 @@ const PartForm = (props: PartFormProps) => {
listClassName="rw-form-error-list"
/>
<Label
name="name"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Name
</Label>
<TextField
name="name"
placeholder="Name"
defaultValue={props.part?.name}
className="rw-input"
errorClassName="rw-input rw-input-error"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
validation={{ required: true }}
/>
<FieldError name="name" className="rw-field-error" />
<FieldError name="name" className="rw-field-error pb-3" />
<Label
name="description"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Description
</Label>
<TextField
<TextAreaField
name="description"
placeholder="Description"
defaultValue={props.part?.description}
className="rw-input"
className="rw-textarea mb-1 min-w-full"
errorClassName="rw-input rw-input-error"
/>
<FieldError name="description" className="rw-field-error" />
<Label
name="availableStock"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Available stock
</Label>
<FieldError name="description" className="rw-field-error pb-3" />
<NumberField
name="availableStock"
defaultValue={props.part?.availableStock}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
defaultValue={props.part?.availableStock ?? 0}
className="rw-input min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
validation={{ required: true, min: 0 }}
min={0}
/>
<FieldError name="availableStock" className="rw-field-error" />
<FieldError name="availableStock" className="rw-field-error pb-3" />
<Label
name="imageUrl"
@ -141,7 +122,7 @@ const PartForm = (props: PartFormProps) => {
<FieldError name="imageUrl" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
<Submit disabled={props.loading} className="rw-button btn-primary">
Save
</Submit>
</div>

View File

@ -37,6 +37,7 @@ const PartsList = ({ parts }: FindParts) => {
}
const thumbnail = (url: string) => {
if (url.includes('no_image.png')) return url
const parts = url.split('/')
parts.splice(3, 0, 'resize=width:100')
return parts.join('/')
@ -85,14 +86,14 @@ const PartsList = ({ parts }: FindParts) => {
<Link
to={routes.editPart({ id: part.id })}
title={'Edit part ' + part.id}
className="rw-button rw-button-small rw-button-blue"
className="rw-button rw-button-small btn-primary"
>
Edit
</Link>
<button
type="button"
title={'Delete part ' + part.id}
className="rw-button rw-button-small rw-button-red"
className="rw-button rw-button-small btn-error"
onClick={() => onDeleteClick(part.id)}
>
Delete

View File

@ -22,8 +22,8 @@ export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
{'No parts yet. '}
<div className="rw-text-center p-4">
<span className="font-inter">No parts yet.</span>{' '}
<Link to={routes.newPart()} className="rw-link">
{'Create one?'}
</Link>

View File

@ -20,12 +20,10 @@ const ScaffoldLayout = ({
<div className="rw-scaffold">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<header className="rw-header">
<h1 className="rw-heading rw-heading-primary">
<Link to={routes[titleTo]()} className="rw-link">
{title}
</Link>
<h1 className="rw-heading rw-heading-primary rw-button btn-ghost normal-case">
<Link to={routes[titleTo]()}>{title}</Link>
</h1>
<Link to={routes[buttonTo]()} className="rw-button rw-button-green">
<Link to={routes[buttonTo]()} className="rw-button btn-success">
<div className="rw-button-icon">+</div> {buttonLabel}
</Link>
</header>

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { Form, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
@ -46,42 +46,38 @@ const ForgotPasswordPage = () => {
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Forgot Password
</h2>
<h2 className="rw-heading rw-heading-primary">Forgot Password</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<div className="text-left">
<Label
name="email"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Email
</Label>
<TextField
name="email"
className="rw-input"
errorClassName="rw-input rw-input-error"
placeholder="Email"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
ref={emailRef}
validation={{
required: {
value: true,
message: 'Email is required',
},
pattern: {
value: new RegExp(
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
),
message: 'Email is not valid',
},
}}
/>
<FieldError name="email" className="rw-field-error" />
<FieldError name="email" className="rw-field-error pb-3" />
</div>
<div className="rw-button-group">
<Submit className="btn btn-primary font-inter">
Submit
</Submit>
<Submit className="rw-button btn-primary">Submit</Submit>
</div>
</Form>
</div>

View File

@ -3,7 +3,6 @@ import { useEffect } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
Submit,
@ -53,23 +52,17 @@ const LoginPage = () => {
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">Login</h2>
<h2 className="rw-heading rw-heading-primary">Login</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<Label
name="email"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Email
</Label>
<TextField
name="email"
className="rw-input"
errorClassName="rw-input rw-input-error"
placeholder="Email"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
ref={emailRef}
validation={{
required: {
@ -79,19 +72,13 @@ const LoginPage = () => {
}}
/>
<FieldError name="email" className="rw-field-error" />
<FieldError name="email" className="rw-field-error pb-3" />
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Password
</Label>
<PasswordField
name="password"
className="rw-input"
errorClassName="rw-input rw-input-error"
placeholder="Password"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
autoComplete="current-password"
validation={{
required: {
@ -113,9 +100,7 @@ const LoginPage = () => {
</div>
<div className="rw-button-group">
<Submit className="btn btn-primary font-inter">
Login
</Submit>
<Submit className="rw-button btn-primary ">Login</Submit>
</div>
</Form>
</div>

View File

@ -1,12 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import {
Form,
Label,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { Form, PasswordField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
@ -66,27 +60,19 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Reset Password
</h2>
<h2 className="rw-heading rw-heading-primary">Reset Password</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<div className="text-left">
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
New Password
</Label>
<PasswordField
name="password"
placeholder="New password"
autoComplete="new-password"
className="rw-input"
errorClassName="rw-input rw-input-error"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
disabled={!enabled}
ref={passwordRef}
validation={{
@ -97,12 +83,15 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
}}
/>
<FieldError name="password" className="rw-field-error" />
<FieldError
name="password"
className="rw-field-error pb-3"
/>
</div>
<div className="rw-button-group">
<Submit
className="btn btn-primary font-inter"
className="rw-button btn-primary"
disabled={!enabled}
>
Submit

View File

@ -3,7 +3,6 @@ import { useEffect } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
FieldError,
@ -31,7 +30,6 @@ const SignupPage = () => {
}, [])
const onSubmit = async (data: Record<string, string>) => {
console.log(data)
const response = await signUp({
username: data.email,
password: data.password,
@ -58,22 +56,16 @@ const SignupPage = () => {
<div className="rw-scaffold rw-login-container">
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">Signup</h2>
<h2 className="rw-heading rw-heading-primary">Sign up</h2>
</header>
<div className="rw-segment-main">
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} className="rw-form-wrapper">
<div className="flex justify-between space-x-3">
<div className="mb-3 flex justify-between space-x-3">
<div>
<Label
name="firstName"
className="mt-6 block text-left font-semibold text-gray-600"
errorClassName="rw-label rw-label-error"
>
First Name
</Label>
<TextField
placeholder="First Name"
name="firstName"
className="rw-input"
errorClassName="rw-input rw-input-error"
@ -89,15 +81,9 @@ const SignupPage = () => {
</div>
<div>
<Label
name="lastName"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Last Name
</Label>
<TextField
name="lastName"
placeholder="Last Name"
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{
@ -111,17 +97,11 @@ const SignupPage = () => {
</div>
</div>
<Label
name="email"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Email
</Label>
<TextField
name="email"
className="rw-input"
errorClassName="rw-input rw-input-error"
placeholder="Email"
className="rw-input mb-3 min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
validation={{
required: {
value: true,
@ -135,19 +115,13 @@ const SignupPage = () => {
},
}}
/>
<FieldError name="email" className="rw-field-error" />
<FieldError name="email" className="rw-field-error pb-3" />
<Label
name="password"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Password
</Label>
<PasswordField
name="password"
className="rw-input"
errorClassName="rw-input rw-input-error"
placeholder="Password"
className="rw-input min-w-full"
errorClassName="rw-input rw-input-error min-w-full"
autoComplete="current-password"
validation={{
required: {
@ -159,9 +133,7 @@ const SignupPage = () => {
<FieldError name="password" className="rw-field-error" />
<div className="rw-button-group">
<Submit className="btn btn-primary font-inter">
Sign Up
</Submit>
<Submit className="rw-button btn-primary">Sign Up</Submit>
</div>
</Form>
</div>

View File

@ -5,9 +5,6 @@
.rw-scaffold h2 {
@apply m-0;
}
.rw-scaffold a {
@apply bg-transparent;
}
.rw-scaffold ul {
@apply m-0 p-0;
}
@ -21,10 +18,10 @@
@apply text-gray-500;
}
.rw-header {
@apply flex justify-between px-8 py-4;
@apply navbar items-center justify-between space-x-3 bg-base-100 shadow-lg;
}
.rw-main {
@apply mx-4 pb-4;
@apply m-4;
}
.rw-segment {
@apply w-full overflow-hidden rounded-lg border border-gray-200;
@ -46,19 +43,19 @@
@apply bg-gray-100 p-4;
}
.rw-link {
@apply text-blue-400 underline font-inter;
@apply font-inter text-blue-400 underline;
}
.rw-link:hover {
@apply text-blue-500;
}
.rw-forgot-link {
@apply mt-1 text-right text-xs text-gray-400 underline font-inter;
@apply mt-1 text-right font-inter text-xs text-gray-400 underline;
}
.rw-forgot-link:hover {
@apply text-blue-500;
}
.rw-heading {
@apply font-semibold font-inter;
@apply font-inter font-semibold;
}
.rw-heading.rw-heading-primary {
@apply text-xl;
@ -76,7 +73,7 @@
@apply text-sm font-semibold;
}
.rw-form-wrapper {
@apply -mt-4 text-sm;
@apply text-sm;
}
.rw-cell-error,
.rw-form-error-wrapper {
@ -89,10 +86,10 @@
@apply mt-2 list-inside list-disc;
}
.rw-button {
@apply btn btn-primary font-inter
@apply btn font-inter hover:shadow-lg;
}
.rw-button.rw-button-small {
@apply rounded-sm px-2 py-1 text-xs;
@apply btn-sm;
}
.rw-button-icon {
@apply mr-1 text-xl leading-5;
@ -107,13 +104,16 @@
@apply mt-8;
}
.rw-label {
@apply mt-6 block text-left font-semibold text-gray-600 font-inter;
@apply mt-6 block text-left font-inter font-semibold text-gray-600;
}
.rw-label.rw-label-error {
@apply text-red-600;
}
.rw-input {
@apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none font-inter;
@apply input input-bordered w-full max-w-xs font-inter;
}
.rw-textarea {
@apply textarea textarea-bordered font-inter max-w-xs w-full;
}
.rw-check-radio-items {
@apply flex justify-items-center;
@ -129,14 +129,14 @@
@apply border-gray-400;
}
.rw-input-error {
@apply border-red-600 text-red-600;
@apply input-error;
}
.rw-input-error:focus {
@apply border-red-600 outline-none;
box-shadow: 0 0 5px #c53030;
}
.rw-field-error {
@apply mt-1 block text-xs font-semibold text-red-600 font-inter text-left;
@apply my-3 block text-left font-inter text-xs font-semibold text-red-600;
}
.rw-table-wrapper-responsive {
@apply overflow-x-auto;
@ -182,25 +182,7 @@
@apply ml-0;
}
.rw-table-actions {
@apply flex h-4 items-center justify-end pr-1;
}
.rw-table-actions .rw-button {
@apply bg-transparent;
}
.rw-table-actions .rw-button:hover {
@apply bg-gray-500 text-white;
}
.rw-table-actions .rw-button-blue {
@apply text-blue-500;
}
.rw-table-actions .rw-button-blue:hover {
@apply bg-blue-500 text-white;
}
.rw-table-actions .rw-button-red {
@apply text-red-600;
}
.rw-table-actions .rw-button-red:hover {
@apply bg-red-600 text-white;
@apply flex h-4 items-center justify-end pr-1 space-x-2;
}
.rw-text-center {
@apply text-center;