Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Postgres Integration #2

Merged
merged 5 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/db/migrations/20231018201753_initial_postgres/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "UserExample" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,

CONSTRAINT "UserExample_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email");
24 changes: 24 additions & 0 deletions api/db/migrations/20231018203604_user_auth/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Warnings:

- You are about to drop the `UserExample` table. If the table is not empty, all the data it contains will be lost.

*/
-- DropTable
DROP TABLE "UserExample";

-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"hashedPassword" TEXT NOT NULL,
"salt" TEXT NOT NULL,
"resetToken" TEXT,
"resetTokenExpiresAt" TIMESTAMP(3),

CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
23 changes: 23 additions & 0 deletions api/db/migrations/20231018213350_add_identity_table/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "Identity" (
"id" SERIAL NOT NULL,
"provider" TEXT NOT NULL,
"uid" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"accessToken" TEXT,
"scope" TEXT,
"lastLoginAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Identity_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Identity_userId_idx" ON "Identity"("userId");

-- CreateIndex
CREATE UNIQUE INDEX "Identity_provider_uid_key" ON "Identity"("provider", "uid");

-- AddForeignKey
ALTER TABLE "Identity" ADD CONSTRAINT "Identity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions api/db/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -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"
32 changes: 26 additions & 6 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
datasource db {
provider = "sqlite"
provider = "postgresql"
url = env("DATABASE_URL")
}

Expand All @@ -10,9 +10,29 @@ generator client {

// Define your own datamodels here and run `yarn redwood prisma migrate dev`
// to create migrations for them and apply to your dev DB.
// TODO: Please remove the following example:
model UserExample {
id Int @id @default(autoincrement())
email String @unique
name String?
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
identities Identity[]
}

model Identity {
id Int @id @default(autoincrement())
provider String
uid String
userId Int
user User @relation(fields: [userId], references: [id])
accessToken String?
scope String?
lastLoginAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([provider, uid])
@@index(userId)
}
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@redwoodjs/api": "6.3.2",
"@redwoodjs/auth-dbauth-api": "6.3.2",
"@redwoodjs/graphql-server": "6.3.2"
}
}
170 changes: 170 additions & 0 deletions api/src/functions/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { DbAuthHandler } from '@redwoodjs/auth-dbauth-api'

import { db } from 'src/lib/db'

export const handler = async (event, context) => {
const forgotPasswordOptions = {
// handler() is invoked after verifying that a user was found with the given
// username. This is where you can send the user an email with a link to
// reset their password. With the default dbAuth routes and field names, the
// URL to reset the password will be:
//
// https://example.com/reset-password?resetToken=${user.resetToken}
//
// Whatever is returned from this function will be returned from
// the `forgotPassword()` function that is destructured from `useAuth()`
// You could use this return value to, for example, show the email
// address in a toast message so the user will know it worked and where
// to look for the email.
handler: (user) => {
return user
},

// 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: 'Username not found',
// if the user somehow gets around client validation
usernameRequired: 'Username is required',
},
}

const loginOptions = {
// 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 for ${username}',
},

// How long a user will remain logged in, in seconds
expires: 60 * 60 * 24 * 365 * 10,
}

const resetPasswordOptions = {
// 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',
},
}

const signupOptions = {
// 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: ({ username, hashedPassword, salt, userAttributes }) => {
return db.user.create({
data: {
email: username,
hashedPassword: hashedPassword,
salt: salt,
// name: userAttributes.name
},
})
},

// 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) => {
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: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},

// 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: {
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',
},

forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})

return await authHandler.invoke()
}
4 changes: 4 additions & 0 deletions api/src/functions/graphql.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { authDecoder } 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 { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
Expand Down
Loading