diff --git a/.env.defaults b/.env.defaults
index 6c3f094..d4c199b 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -3,9 +3,6 @@
# system. Any custom values should go in .env and .env should *not* be checked
# into version control.
-# schema.prisma defaults
-DATABASE_URL=file:./dev.db
-
# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
# TEST_DATABASE_URL=file:./.redwood/test.db
@@ -18,4 +15,15 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
# Ordered by how verbose they are: trace | debug | info | warn | error | silent
# LOG_LEVEL=debug
+NAME=Ahmed Al-Taiar
+
+GMAIL=example@gmail.com
+GMAIL_SMTP_PASSWORD=chan geme xyza bcde
+
+# Must not end with "/"
+DOMAIN_PROD=https://example.com
+DOMAIN_DEV=http://localhost:8910
+
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
+
+SESSION_SECRET=regenerate_me
diff --git a/.env.example b/.env.example
index 18a1759..b48244f 100644
--- a/.env.example
+++ b/.env.example
@@ -3,4 +3,15 @@
# PRISMA_HIDE_UPDATE_MESSAGE=true
# LOG_LEVEL=trace
+NAME=Firstname Lastname
+
+GMAIL=example@gmail.com
+GMAIL_SMTP_PASSWORD=chan geme xyza bcde
+
+# Must not end with "/"
+DOMAIN_PROD=https://example.com
+DOMAIN_DEV=http://localhost:8910
+
DATABASE_URL=postgresql://user:password@localhost:5432/rw_portfolio
+
+SESSION_SECRET=regenerate_me
diff --git a/api/db/migrations/20240810184713_user/migration.sql b/api/db/migrations/20240810184713_user/migration.sql
new file mode 100644
index 0000000..6bcd44f
--- /dev/null
+++ b/api/db/migrations/20240810184713_user/migration.sql
@@ -0,0 +1,18 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "username" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" TIMESTAMP(3),
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/api/db/migrations/migration_lock.toml b/api/db/migrations/migration_lock.toml
new file mode 100644
index 0000000..fbffa92
--- /dev/null
+++ b/api/db/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/api/db/schema.prisma b/api/db/schema.prisma
index 80da5a1..80e79b9 100644
--- a/api/db/schema.prisma
+++ b/api/db/schema.prisma
@@ -13,3 +13,13 @@ generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
+
+model User {
+ id Int @id @default(autoincrement())
+ username String @unique
+ email String @unique
+ hashedPassword String
+ salt String
+ resetToken String?
+ resetTokenExpiresAt DateTime?
+}
diff --git a/api/package.json b/api/package.json
index 250f70e..86a587c 100644
--- a/api/package.json
+++ b/api/package.json
@@ -3,7 +3,12 @@
"version": "0.0.0",
"private": true,
"dependencies": {
- "@redwoodjs/api": "7.7.3",
- "@redwoodjs/graphql-server": "7.7.3"
+ "@redwoodjs/api": "7.7.4",
+ "@redwoodjs/auth-dbauth-api": "7.7.4",
+ "@redwoodjs/graphql-server": "7.7.4",
+ "nodemailer": "^6.9.14"
+ },
+ "devDependencies": {
+ "@types/nodemailer": "^6.4.15"
}
}
diff --git a/api/src/functions/auth.ts b/api/src/functions/auth.ts
new file mode 100644
index 0000000..977450f
--- /dev/null
+++ b/api/src/functions/auth.ts
@@ -0,0 +1,231 @@
+import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
+
+import {
+ DbAuthHandler,
+ PasswordValidationError,
+} from '@redwoodjs/auth-dbauth-api'
+import type { DbAuthHandlerOptions, UserType } from '@redwoodjs/auth-dbauth-api'
+
+import { cookieName } from 'src/lib/auth'
+import { db } from 'src/lib/db'
+import { censorEmail, sendEmail } from 'src/lib/email'
+
+export const handler = async (
+ event: APIGatewayProxyEvent,
+ context: Context
+) => {
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
+ handler: async (user, resetToken) => {
+ const domain =
+ (process.env.NODE_ENV || 'development') == 'production'
+ ? process.env.DOMAIN_PROD
+ : process.env.DOMAIN_DEV
+
+ const text = `If this wasn't you, please disregard this email.
+
+Hello,
+
+You are receiving this email because a password reset was requested.
+Enter the following URL to begin resetting your password:
+
+${domain}/reset-password?resetToken=${resetToken}
+ `
+
+ const html = text.replaceAll('\n', ' ')
+
+ try {
+ await sendEmail({
+ to: user.email,
+ subject: 'Password Reset Request',
+ text,
+ html,
+ })
+
+ return {
+ email: censorEmail(user.email),
+ }
+ } catch (error) {
+ return {
+ error: `Error: ${error.code} ${error.responseCode}`,
+ }
+ }
+ },
+
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
+ expires: 60 * 60 * 24,
+
+ errors: {
+ // for security reasons you may want to be vague here rather than expose
+ // the fact that the email address wasn't found (prevents fishing for
+ // valid email addresses)
+ usernameNotFound: 'Account not found',
+ // if the user somehow gets around client validation
+ usernameRequired: 'Email is required',
+ },
+ }
+
+ const loginOptions: DbAuthHandlerOptions['login'] = {
+ // handler() is called after finding the user that matches the
+ // username/password provided at login, but before actually considering them
+ // logged in. The `user` argument will be the user in the database that
+ // matched the username/password.
+ //
+ // If you want to allow this user to log in simply return the user.
+ //
+ // If you want to prevent someone logging in for another reason (maybe they
+ // didn't validate their email yet), throw an error and it will be returned
+ // by the `logIn()` function from `useAuth()` in the form of:
+ // `{ message: 'Error message' }`
+ handler: (user) => {
+ return user
+ },
+
+ errors: {
+ usernameOrPasswordMissing: 'Both username and password are required',
+ usernameNotFound: 'Username ${username} not found',
+ // For security reasons you may want to make this the same as the
+ // usernameNotFound error so that a malicious user can't use the error
+ // to narrow down if it's the username or password that's incorrect
+ incorrectPassword: 'Incorrect password',
+ },
+
+ // How long a user will remain logged in, in seconds
+ expires: 60 * 60 * 24 * 365 * 10,
+ }
+
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
+ // handler() is invoked after the password has been successfully updated in
+ // the database. Returning anything truthy will automatically log the user
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
+ // user to the login page.
+ handler: (_user) => {
+ return true
+ },
+
+ // If `false` then the new password MUST be different from the current one
+ allowReusedPassword: true,
+
+ errors: {
+ // the resetToken is valid, but expired
+ resetTokenExpired: 'resetToken is expired',
+ // no user was found with the given resetToken
+ resetTokenInvalid: 'resetToken is invalid',
+ // the resetToken was not present in the URL
+ resetTokenRequired: 'resetToken is required',
+ // new password is the same as the old password (apparently they did not forget it)
+ reusedPassword: 'Must choose a new password',
+ },
+ }
+
+ interface UserAttributes {
+ email: string
+ }
+
+ const signupOptions: DbAuthHandlerOptions<
+ UserType,
+ UserAttributes
+ >['signup'] = {
+ // Whatever you want to happen to your data on new user signup. Redwood will
+ // check for duplicate usernames before calling this handler. At a minimum
+ // you need to save the `username`, `hashedPassword` and `salt` to your
+ // user table. `userAttributes` contains any additional object members that
+ // were included in the object given to the `signUp()` function you got
+ // from `useAuth()`.
+ //
+ // If you want the user to be immediately logged in, return the user that
+ // was created.
+ //
+ // If this handler throws an error, it will be returned by the `signUp()`
+ // function in the form of: `{ error: 'Error message' }`.
+ //
+ // If this returns anything else, it will be returned by the
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
+ handler: async ({ username, hashedPassword, salt, userAttributes }) => {
+ // Only one admin account should exist
+ if ((await db.user.count()) >= 1)
+ return { error: 'Admin account already exists' }
+
+ if (
+ new RegExp(
+ /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
+ ).test(userAttributes.email)
+ )
+ return db.user.create({
+ data: {
+ username: username,
+ email: userAttributes.email,
+ hashedPassword: hashedPassword,
+ salt: salt,
+ },
+ })
+ else return { error: 'Invalid email' }
+ },
+
+ // Include any format checks for password here. Return `true` if the
+ // password is valid, otherwise throw a `PasswordValidationError`.
+ // Import the error along with `DbAuthHandler` from `@redwoodjs/api` above.
+ passwordValidation: (password) => {
+ if (password.length < 8)
+ throw new PasswordValidationError(
+ 'Password must be at least 8 characters'
+ )
+ else return true
+ },
+
+ errors: {
+ // `field` will be either "username" or "password"
+ fieldMissing: '${field} is required',
+ usernameTaken: 'Username `${username}` already in use',
+ },
+ }
+
+ const authHandler = new DbAuthHandler(event, context, {
+ // Provide prisma db client
+ db: db,
+
+ // The name of the property you'd call on `db` to access your user table.
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
+ authModelAccessor: 'user',
+
+ // A map of what dbAuth calls a field to what your database calls it.
+ // `id` is whatever column you use to uniquely identify a user (probably
+ // something like `id` or `userId` or even `email`)
+ authFields: {
+ id: 'id',
+ username: 'username',
+ hashedPassword: 'hashedPassword',
+ salt: 'salt',
+ resetToken: 'resetToken',
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
+ },
+
+ // A list of fields on your user object that are safe to return to the
+ // client when invoking a handler that returns a user (like forgotPassword
+ // and signup). This list should be as small as possible to be sure not to
+ // leak any sensitive information to the client.
+ allowedUserFields: ['id', 'email'],
+
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
+ cookie: {
+ attributes: {
+ HttpOnly: true,
+ Path: '/',
+ SameSite: 'Strict',
+ Secure: process.env.NODE_ENV !== 'development',
+
+ // If you need to allow other domains (besides the api side) access to
+ // the dbAuth session cookie:
+ // Domain: 'example.com',
+ },
+ name: cookieName,
+ },
+
+ forgotPassword: forgotPasswordOptions,
+ login: loginOptions,
+ resetPassword: resetPasswordOptions,
+ signup: signupOptions,
+ })
+
+ return await authHandler.invoke()
+}
diff --git a/api/src/functions/graphql.ts b/api/src/functions/graphql.ts
index f395c3b..e9c53e2 100644
--- a/api/src/functions/graphql.ts
+++ b/api/src/functions/graphql.ts
@@ -1,13 +1,19 @@
+import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
+import { cookieName, getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
+const authDecoder = createAuthDecoder(cookieName)
+
export const handler = createGraphQLHandler({
+ authDecoder,
+ getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts
new file mode 100644
index 0000000..7db79bd
--- /dev/null
+++ b/api/src/graphql/users.sdl.ts
@@ -0,0 +1,15 @@
+export const schema = gql`
+ type User {
+ id: Int!
+ username: String!
+ email: String!
+ hashedPassword: String!
+ salt: String!
+ resetToken: String
+ resetTokenExpiresAt: DateTime
+ }
+
+ type Query {
+ userCount: Int! @skipAuth
+ }
+`
diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts
index 4b0b887..d6af4c5 100644
--- a/api/src/lib/auth.ts
+++ b/api/src/lib/auth.ts
@@ -1,32 +1,117 @@
+import type { Decoded } from '@redwoodjs/api'
+import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
+
+import { db } from './db'
+
/**
- * Once you are ready to add authentication to your application
- * you'll build out requireAuth() with real functionality. For
- * now we just return `true` so that the calls in services
- * have something to check against, simulating a logged
- * in user that is allowed to access that service.
+ * The name of the cookie that dbAuth sets
*
- * See https://redwoodjs.com/docs/authentication for more info.
+ * %port% will be replaced with the port the api server is running on.
+ * If you have multiple RW apps running on the same host, you'll need to
+ * make sure they all use unique cookie names
*/
-export const isAuthenticated = () => {
- return true
+export const cookieName = 'session_%port%'
+
+/**
+ * The session object sent in as the first argument to getCurrentUser() will
+ * have a single key `id` containing the unique ID of the logged in user
+ * (whatever field you set as `authFields.id` in your auth function config).
+ * You'll need to update the call to `db` below if you use a different model
+ * name or unique field name, for example:
+ *
+ * return await db.profile.findUnique({ where: { email: session.id } })
+ * ───┬─── ──┬──
+ * model accessor ─┘ unique id field name ─┘
+ *
+ * !! BEWARE !! Anything returned from this function will be available to the
+ * client--it becomes the content of `currentUser` on the web side (as well as
+ * `context.currentUser` on the api side). You should carefully add additional
+ * fields to the `select` object below once you've decided they are safe to be
+ * seen if someone were to open the Web Inspector in their browser.
+ */
+export const getCurrentUser = async (session: Decoded) => {
+ if (!session || typeof session.id !== 'number') {
+ throw new Error('Invalid session')
+ }
+
+ return await db.user.findUnique({
+ where: { id: session.id },
+ select: { id: true },
+ })
}
-export const hasRole = ({ roles }) => {
- return roles !== undefined
+/**
+ * The user is authenticated if there is a currentUser in the context
+ *
+ * @returns {boolean} - If the currentUser is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return !!context.currentUser
}
-// This is used by the redwood directive
-// in ./api/src/directives/requireAuth
+/**
+ * When checking role membership, roles can be a single value, a list, or none.
+ * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
+ */
+type AllowedRoles = string | string[] | undefined
-// Roles are passed in by the requireAuth directive if you have auth setup
-// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
-export const requireAuth = ({ roles }) => {
- return isAuthenticated()
+/**
+ * Checks if the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
+ *
+ * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
+ * or when no roles are provided to check against. Otherwise returns false.
+ */
+export const hasRole = (roles: AllowedRoles): boolean => {
+ if (!isAuthenticated()) return false
+
+ const currentUserRoles = context.currentUser?.roles
+
+ if (typeof roles === 'string') {
+ // roles to check is a string, currentUser.roles is a string
+ if (typeof currentUserRoles === '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)
+ }
+ }
+
+ // roles not found
+ return false
}
-export const getCurrentUser = async () => {
- throw new Error(
- 'Auth is not set up yet. See https://redwoodjs.com/docs/authentication ' +
- 'to get started'
- )
+/**
+ * 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
+ * error if they're not.
+ *
+ * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
+ *
+ * @returns - If the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @throws {@link AuthenticationError} - If the currentUser is not authenticated
+ * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
+ *
+ * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
+ */
+export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
+ if (!isAuthenticated()) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
+
+ if (roles && !hasRole(roles)) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
}
diff --git a/api/src/lib/email.ts b/api/src/lib/email.ts
new file mode 100644
index 0000000..246b58d
--- /dev/null
+++ b/api/src/lib/email.ts
@@ -0,0 +1,39 @@
+import * as nodemailer from 'nodemailer'
+
+interface Options {
+ to: string | string[]
+ subject: string
+ html: string
+ text: string
+}
+
+const transporter = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.GMAIL,
+ pass: process.env.GMAIL_SMTP_PASSWORD,
+ },
+})
+
+export async function sendEmail({ to, subject, text, html }: Options) {
+ return await transporter.sendMail({
+ from: `"${process.env.NAME} (noreply)" <${process.env.GMAIL}>`,
+ to: Array.isArray(to) ? to : [to],
+ subject,
+ text,
+ html,
+ })
+}
+
+export function censorEmail(email: string): string {
+ const [localPart, domain] = email.split('@')
+
+ if (localPart.length <= 2) return `${localPart}@${domain}`
+
+ const firstChar = localPart[0]
+ const lastChar = localPart[localPart.length - 1]
+ const middleLength = Math.min(localPart.length - 2, 7)
+ const middle = '∗'.repeat(middleLength)
+
+ return `${firstChar}${middle}${lastChar}@${domain}`
+}
diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts
new file mode 100644
index 0000000..a708d87
--- /dev/null
+++ b/api/src/services/users/users.ts
@@ -0,0 +1,36 @@
+import type { QueryResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const userCount: QueryResolvers['userCount'] = () => {
+ return db.user.count()
+}
+
+// export const users: QueryResolvers['users'] = () => {
+// return db.user.findMany()
+// }
+
+// export const user: QueryResolvers['user'] = ({ id }) => {
+// return db.user.findUnique({
+// where: { id },
+// })
+// }
+
+// export const createUser: MutationResolvers['createUser'] = ({ input }) => {
+// return db.user.create({
+// data: input,
+// })
+// }
+
+// export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => {
+// return db.user.update({
+// data: input,
+// where: { id },
+// })
+// }
+
+// export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => {
+// return db.user.delete({
+// where: { id },
+// })
+// }
diff --git a/package.json b/package.json
index 7ec68f8..548bb23 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,9 @@
]
},
"devDependencies": {
- "@redwoodjs/core": "7.7.3",
- "@redwoodjs/project-config": "7.7.3",
+ "@redwoodjs/auth-dbauth-setup": "7.7.4",
+ "@redwoodjs/core": "7.7.4",
+ "@redwoodjs/project-config": "7.7.4",
"prettier-plugin-tailwindcss": "0.4.1"
},
"eslintConfig": {
diff --git a/redwood.toml b/redwood.toml
index d2af1a9..1fc7205 100644
--- a/redwood.toml
+++ b/redwood.toml
@@ -6,13 +6,13 @@
# https://redwoodjs.com/docs/app-configuration-redwood-toml
[web]
- title = "Ahmed Al-Taiar"
+ title = "${NAME}"
port = 8910
apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
- includeEnvironmentVariables = [
- # Add any ENV vars that should be available to the web side to this array
- # See https://redwoodjs.com/docs/environment-variables#web
- ]
+ includeEnvironmentVariables = ["NAME"]
+[generate]
+ tests = false
+ stories = false
[api]
port = 8911
[browser]
diff --git a/web/package.json b/web/package.json
index e2b64dc..22c68db 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,14 +13,15 @@
"dependencies": {
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
- "@redwoodjs/forms": "7.7.3",
- "@redwoodjs/router": "7.7.3",
- "@redwoodjs/web": "7.7.3",
+ "@redwoodjs/auth-dbauth-web": "7.7.4",
+ "@redwoodjs/forms": "7.7.4",
+ "@redwoodjs/router": "7.7.4",
+ "@redwoodjs/web": "7.7.4",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
- "@redwoodjs/vite": "7.7.3",
+ "@redwoodjs/vite": "7.7.4",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"autoprefixer": "^10.4.20",
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 97fb5e0..f636dc8 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -4,14 +4,19 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
+import './scaffold.css'
+import { AuthProvider, useAuth } from './auth'
+
import './index.css'
const App = () => (
-
-
-
+
+
+
+
+
)
diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx
index 5a60174..7b98703 100644
--- a/web/src/Routes.tsx
+++ b/web/src/Routes.tsx
@@ -1,19 +1,28 @@
-// In this file, all Page components from 'src/pages` are auto-imported. Nested
-// directories are supported, and should be uppercase. Each subdirectory will be
-// prepended onto the component name.
-//
-// Examples:
-//
-// 'src/pages/HomePage/HomePage.js' -> HomePage
-// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
-
import { Router, Route, Set } from '@redwoodjs/router'
+import { useAuth } from './auth'
+import AccountbarLayout from './layouts/AccountbarLayout/AccountbarLayout'
import NavbarLayout from './layouts/NavbarLayout/NavbarLayout'
const Routes = () => {
return (
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/auth.ts b/web/src/auth.ts
new file mode 100644
index 0000000..143e75b
--- /dev/null
+++ b/web/src/auth.ts
@@ -0,0 +1,5 @@
+import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/web/src/components/ThemeToggle/ThemeToggle.stories.tsx b/web/src/components/ThemeToggle/ThemeToggle.stories.tsx
deleted file mode 100644
index 910790e..0000000
--- a/web/src/components/ThemeToggle/ThemeToggle.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-// Pass props to your component by passing an `args` object to your story
-//
-// ```tsx
-// export const Primary: Story = {
-// args: {
-// propName: propValue
-// }
-// }
-// ```
-//
-// See https://storybook.js.org/docs/react/writing-stories/args.
-
-import type { Meta, StoryObj } from '@storybook/react'
-
-import ThemeToggle from './ThemeToggle'
-
-const meta: Meta = {
- component: ThemeToggle,
- tags: ['autodocs'],
-}
-
-export default meta
-
-type Story = StoryObj
-
-export const Primary: Story = {}
diff --git a/web/src/components/ThemeToggle/ThemeToggle.test.tsx b/web/src/components/ThemeToggle/ThemeToggle.test.tsx
deleted file mode 100644
index 09642ab..0000000
--- a/web/src/components/ThemeToggle/ThemeToggle.test.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { render } from '@redwoodjs/testing/web'
-
-import ThemeToggle from './ThemeToggle'
-
-// Improve this test with help from the Redwood Testing Doc:
-// https://redwoodjs.com/docs/testing#testing-components
-
-describe('ThemeToggle', () => {
- it('renders successfully', () => {
- expect(() => {
- render()
- }).not.toThrow()
- })
-})
diff --git a/web/src/components/ThemeToggle/ThemeToggle.tsx b/web/src/components/ThemeToggle/ThemeToggle.tsx
index 18b3615..3a7bc0d 100644
--- a/web/src/components/ThemeToggle/ThemeToggle.tsx
+++ b/web/src/components/ThemeToggle/ThemeToggle.tsx
@@ -9,11 +9,8 @@ const ThemeToggle = () => {
)
const handleToggle = (e) => {
- if (e.target.checked) {
- setTheme('dark')
- } else {
- setTheme('light')
- }
+ if (e.target.checked) setTheme('dark')
+ else setTheme('light')
}
useEffect(() => {
diff --git a/web/src/components/ToastNotification/ToastNotification.stories.tsx b/web/src/components/ToastNotification/ToastNotification.stories.tsx
deleted file mode 100644
index 270318f..0000000
--- a/web/src/components/ToastNotification/ToastNotification.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-// Pass props to your component by passing an `args` object to your story
-//
-// ```tsx
-// export const Primary: Story = {
-// args: {
-// propName: propValue
-// }
-// }
-// ```
-//
-// See https://storybook.js.org/docs/react/writing-stories/args.
-
-import type { Meta, StoryObj } from '@storybook/react'
-
-import ToastNotification from './ToastNotification'
-
-const meta: Meta = {
- component: ToastNotification,
- tags: ['autodocs'],
-}
-
-export default meta
-
-type Story = StoryObj
-
-export const Primary: Story = {}
diff --git a/web/src/components/ToastNotification/ToastNotification.test.tsx b/web/src/components/ToastNotification/ToastNotification.test.tsx
deleted file mode 100644
index d87dfb3..0000000
--- a/web/src/components/ToastNotification/ToastNotification.test.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { render } from '@redwoodjs/testing/web'
-
-import ToastNotification from './ToastNotification'
-
-// Improve this test with help from the Redwood Testing Doc:
-// https://redwoodjs.com/docs/testing#testing-components
-
-describe('Toast', () => {
- it('renders successfully', () => {
- expect(() => {
- render()
- }).not.toThrow()
- })
-})
diff --git a/web/src/components/ToastNotification/ToastNotification.tsx b/web/src/components/ToastNotification/ToastNotification.tsx
index 25d28a7..3a25567 100644
--- a/web/src/components/ToastNotification/ToastNotification.tsx
+++ b/web/src/components/ToastNotification/ToastNotification.tsx
@@ -37,22 +37,24 @@ const ToastNotification = ({ t, type, message }: Props) => {
- {iconElement}
-
{message}
+
{iconElement}
+
+
{message}
+
{type !== 'loading' ? (
-
- ) : (
- <>>
- )}
+
+
+
+ ) : null}
)
}
diff --git a/web/src/components/ToasterWrapper/ToasterWrapper.tsx b/web/src/components/ToasterWrapper/ToasterWrapper.tsx
new file mode 100644
index 0000000..1459c8e
--- /dev/null
+++ b/web/src/components/ToasterWrapper/ToasterWrapper.tsx
@@ -0,0 +1,20 @@
+import { Toaster } from '@redwoodjs/web/dist/toast'
+
+import ToastNotification from '../ToastNotification/ToastNotification'
+
+const ToasterWrapper = () => (
+
+ {(t) => (
+
+ )}
+
+)
+
+export default ToasterWrapper
diff --git a/web/src/layouts/AccountbarLayout/AccountbarLayout.stories.tsx b/web/src/layouts/AccountbarLayout/AccountbarLayout.stories.tsx
new file mode 100644
index 0000000..aa37ce8
--- /dev/null
+++ b/web/src/layouts/AccountbarLayout/AccountbarLayout.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import AccountbarLayout from './AccountbarLayout'
+
+const meta: Meta = {
+ component: AccountbarLayout,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/web/src/pages/HomePage/HomePage.test.tsx b/web/src/layouts/AccountbarLayout/AccountbarLayout.test.tsx
similarity index 68%
rename from web/src/pages/HomePage/HomePage.test.tsx
rename to web/src/layouts/AccountbarLayout/AccountbarLayout.test.tsx
index c684c7a..25c93a4 100644
--- a/web/src/pages/HomePage/HomePage.test.tsx
+++ b/web/src/layouts/AccountbarLayout/AccountbarLayout.test.tsx
@@ -1,14 +1,14 @@
import { render } from '@redwoodjs/testing/web'
-import HomePage from './HomePage'
+import AccountbarLayout from './AccountbarLayout'
// Improve this test with help from the Redwood Testing Doc:
// https://redwoodjs.com/docs/testing#testing-pages-layouts
-describe('HomePage', () => {
+describe('AccountbarLayout', () => {
it('renders successfully', () => {
expect(() => {
- render()
+ render()
}).not.toThrow()
})
})
diff --git a/web/src/layouts/AccountbarLayout/AccountbarLayout.tsx b/web/src/layouts/AccountbarLayout/AccountbarLayout.tsx
new file mode 100644
index 0000000..7c3bcd7
--- /dev/null
+++ b/web/src/layouts/AccountbarLayout/AccountbarLayout.tsx
@@ -0,0 +1,35 @@
+import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
+import ToasterWrapper from 'src/components/ToasterWrapper/ToasterWrapper'
+
+type AccountbarLayoutProps = {
+ title: string
+ children?: React.ReactNode
+}
+
+const AccountbarLayout = ({ title, children }: AccountbarLayoutProps) => {
+ return (
+ <>
+
+
+
+
+
{title}
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {children}
+
+ >
+ )
+}
+
+export default AccountbarLayout
diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
index f888d04..9e986fe 100644
--- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx
+++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
@@ -1,11 +1,11 @@
-import { mdiMenu } from '@mdi/js'
+import { mdiMenu, mdiLogout } from '@mdi/js'
import Icon from '@mdi/react'
import { Link, routes } from '@redwoodjs/router'
-import { Toaster } from '@redwoodjs/web/toast'
+import { useAuth } from 'src/auth'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
-import ToastNotification from 'src/components/ToastNotification/ToastNotification'
+import ToasterWrapper from 'src/components/ToasterWrapper/ToasterWrapper'
interface NavbarRoute {
name: string
@@ -17,6 +17,8 @@ type NavbarLayoutProps = {
}
const NavbarLayout = ({ children }: NavbarLayoutProps) => {
+ const { isAuthenticated, logOut } = useAuth()
+
// TODO: populate with buttons to other page
const navbarRoutes: NavbarRoute[] = [
{
@@ -34,22 +36,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
return (
<>
-
- {(t) => (
-
- )}
-
+