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

Injected auth models #1583

Merged
merged 66 commits into from
Jan 3, 2024
Merged
Changes from 1 commit
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
87a24b1
WIP: Auth entity
infomiho Nov 15, 2023
7749dec
Merge branch 'main' into auth-model-experiment
infomiho Nov 15, 2023
a7e3c14
Username and password working
infomiho Nov 15, 2023
bf77d11
Add support for email auth
infomiho Nov 15, 2023
238c3a8
Add support for Social auth
infomiho Nov 15, 2023
627ecf9
Add tasks to User
infomiho Nov 16, 2023
4259689
Update authentication providers and entities
infomiho Nov 24, 2023
20d4fd4
Email authentication. Test adding signup fields
infomiho Nov 24, 2023
33eb3aa
Migration with seed scripts
infomiho Nov 24, 2023
993e39d
Migrate Todo app
infomiho Nov 24, 2023
cf38191
Clenaup
infomiho Nov 27, 2023
9dfbd6c
Remove example app
infomiho Nov 27, 2023
4d0e3e4
Cleanup
infomiho Nov 27, 2023
a768c36
Updates tests
infomiho Nov 27, 2023
83ad555
Cleanup
infomiho Nov 27, 2023
8693c69
Fixes tests
infomiho Nov 28, 2023
43fb9f7
Merge branch 'main' into auth-model-experiment
infomiho Dec 12, 2023
e6f6a17
Use JSON based auth model
infomiho Dec 12, 2023
2714731
Refactor provider data serialization
infomiho Dec 12, 2023
872bca1
Updates typing of provider data
infomiho Dec 13, 2023
fd87def
Remove Prisma middleware. Fixes types
infomiho Dec 13, 2023
3ae0468
Cleanup
infomiho Dec 13, 2023
b5e0c05
Fixes double password hashing issue
infomiho Dec 13, 2023
315962f
Fixes headless test
infomiho Dec 13, 2023
95bda0a
Update e2e tests
infomiho Dec 13, 2023
efc2291
Cleanup
infomiho Dec 13, 2023
7fa0fe1
Merge branch 'main' into auth-model-experiment
infomiho Dec 14, 2023
21b0e77
Updates utils.ts. Updates websocket example app
infomiho Dec 14, 2023
f94f7fd
Update e2e tests
infomiho Dec 14, 2023
0a1d965
Update examples apps. Update server utils.ts
infomiho Dec 14, 2023
52f0881
PR comments
infomiho Dec 18, 2023
f982e6d
Updates e2e tests
infomiho Dec 18, 2023
48484c1
Add user ID helpers
infomiho Dec 18, 2023
6ba6a21
Fixes e2e tests
infomiho Dec 18, 2023
cf19e38
Improve naming and types
infomiho Dec 19, 2023
c5aee00
Updates e2e tests
infomiho Dec 19, 2023
a3cb241
Update examples/waspello/src/client/Navbar.jsx
infomiho Dec 19, 2023
b0b1a8b
PR comments
infomiho Dec 21, 2023
ce1c89b
PR comments
infomiho Dec 21, 2023
c3e46dc
Minor fixes. Rename local provider to username
infomiho Dec 22, 2023
c58ed9c
Updates e2e tests
infomiho Dec 22, 2023
2429673
Fixes frontend unit tests
infomiho Dec 22, 2023
d71d0f7
PR comments
infomiho Dec 22, 2023
8d27abb
Update e2e tests
infomiho Dec 22, 2023
d9690bb
Improve username handling in examples. Add getFirstProviderUserId
infomiho Dec 22, 2023
226e38b
Update e2e tests
infomiho Dec 22, 2023
9303dbc
Update seed script path
infomiho Dec 22, 2023
7ea4792
Add comment above PossibleProviderData
infomiho Dec 22, 2023
887ce20
Use UTC date for auth related timings
infomiho Dec 22, 2023
215bb86
Updates e2e tests
infomiho Dec 22, 2023
227a2f8
unverfiied email signup flow. foregin constraint error
infomiho Dec 23, 2023
1c53057
e2e tests
infomiho Dec 23, 2023
5daf665
Use token in email verification and password reset
infomiho Dec 23, 2023
4d1a863
e2e tests
infomiho Dec 23, 2023
dba9d28
Fix user creation and deletion errors
infomiho Dec 27, 2023
c313736
e2e tests
infomiho Dec 27, 2023
acf5d1f
PR comments
infomiho Dec 29, 2023
49966c0
PR comments
infomiho Jan 2, 2024
7c1b66e
Update e2e tests
infomiho Jan 2, 2024
04a9ee1
Extract the oauth handler into a separate function
infomiho Jan 2, 2024
d84dcc9
e2e tests
infomiho Jan 2, 2024
d4339a4
Update waspc/data/Generator/templates/server/src/auth/providers/email…
infomiho Jan 3, 2024
5077add
Apply suggestions from code review
infomiho Jan 3, 2024
9e4d7bb
Merge branch 'main' into auth-model-experiment
infomiho Jan 3, 2024
993eeac
PR comments
infomiho Jan 3, 2024
9b1185d
e2e tests
infomiho Jan 3, 2024
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
Prev Previous commit
Next Next commit
Username and password working
infomiho committed Nov 15, 2023
commit a7e3c1477a515ff97fba0c7cf56ff494a4f28850
5 changes: 4 additions & 1 deletion waspc/data/Generator/templates/server/src/_types/index.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } fr
import prisma from "../dbClient.js"
{=# isAuthEnabled =}
import { type {= userEntityName =} } from "../entities"
import { type {= authEntityName =} } from "@prisma/client"
{=/ isAuthEnabled =}
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
@@ -83,5 +84,7 @@ type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & {
// password field from the object there, we must do the same here). Ideally,
// these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
export type SanitizedUser = Omit<{= userEntityName =}, 'password'>
export type SanitizedUser = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: Omit<{= authEntityName =}, 'password'>
}
{=/ isAuthEnabled =}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { verifyPassword, throwInvalidCredentialsError } from "../../../core/auth.js";
import { findUserBy, createAuthToken } from "../../utils.js";
import { findAuthWithUserBy, createAuthToken } from "../../utils.js";
import { ensureValidEmail, ensurePasswordIsPresent } from "../../validation.js";

export function getLoginRoute({
@@ -17,7 +17,7 @@ export function getLoginRoute({

userFields.email = userFields.email.toLowerCase()

const user = await findUserBy({ email: userFields.email })
const user = await findAuthWithUserBy({ email: userFields.email })
if (!user) {
throwInvalidCredentialsError()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import {
findUserBy,
findAuthWithUserBy,
doFakeWork,
} from "../../utils.js";
import {
@@ -30,7 +30,7 @@ export function getRequestPasswordResetRoute({

args.email = args.email.toLowerCase();

const user = await findUserBy({ email: args.email });
const user = await findAuthWithUserBy({ email: args.email });

// User not found or not verified - don't leak information
infomiho marked this conversation as resolved.
Show resolved Hide resolved
if (!user || !user.isEmailVerified) {
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { findUserBy, verifyToken } from "../../utils.js";
import { findAuthWithUserBy, verifyToken } from "../../utils.js";
import { updateUserPassword } from "./utils.js";
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
import { tokenVerificationErrors } from "./types.js";
@@ -14,7 +14,7 @@ export async function resetPassword(
const { token, password } = args;
try {
const { id: userId } = await verifyToken(token);
const user = await findUserBy({ id: userId });
const user = await findAuthWithUserBy({ id: userId });
if (!user) {
return res.status(400).json({ success: false, message: 'Invalid token' });
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { EmailFromField } from "../../../email/core/types.js";
import {
createUser,
findUserBy,
findAuthWithUserBy,
deleteUser,
doFakeWork,
} from "../../utils.js";
@@ -33,7 +33,7 @@ export function getSignupRoute({

userFields.email = userFields.email.toLowerCase();

const existingUser = await findUserBy({ email: userFields.email });
const existingUser = await findAuthWithUserBy({ email: userFields.email });
// User already exists and is verified - don't leak information
if (existingUser && existingUser.isEmailVerified) {
await doFakeWork();
@@ -47,11 +47,13 @@ export function getSignupRoute({

const additionalFields = await validateAndGetAdditionalFields(userFields);

const user = await createUser({
...additionalFields,
email: userFields.email,
password: userFields.password,
});
const user = await createUser(
{
email: userFields.email,
password: userFields.password,
},
additionalFields,
);

const verificationLink = await createEmailVerificationLink(user, clientRoute);
try {
Original file line number Diff line number Diff line change
@@ -14,4 +14,4 @@ export const tokenVerificationErrors = {
TokenExpiredError: 'TokenExpiredError',
};

export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"email" | "password">()
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn()
Original file line number Diff line number Diff line change
@@ -2,26 +2,26 @@
import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js'
import { handleRejection } from '../../../utils.js'

import { findUserBy, createAuthToken } from '../../utils.js'
import { findAuthWithUserBy, createAuthToken } from '../../utils.js'
import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js'

export default handleRejection(async (req, res) => {
const userFields = req.body || {}
ensureValidArgs(userFields)

const user = await findUserBy({ username: userFields.username })
if (!user) {
const auth = await findAuthWithUserBy({ username: userFields.username })
if (!auth) {
throwInvalidCredentialsError()
}

try {
await verifyPassword(user.password, userFields.password)
await verifyPassword(auth.password, userFields.password)
} catch(e) {
throwInvalidCredentialsError()
}

// Username & password valid - generate token.
const token = await createAuthToken(user)
const token = await createAuthToken(auth)

// NOTE(matija): Possible option - instead of explicitly returning token here,
// we could add to response header 'Set-Cookie {token}' directive which would then make
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{{={= =}=}}
import { handleRejection } from '../../../utils.js'
import { createUser } from '../../utils.js'
import { ensureValidUsername, ensurePasswordIsPresent, ensureValidPassword } from '../../validation.js'
import {
ensureValidUsername,
ensurePasswordIsPresent,
ensureValidPassword,
} from '../../validation.js'
import { validateAndGetAdditionalFields } from '../../utils.js'

export default handleRejection(async (req, res) => {
@@ -10,17 +14,19 @@ export default handleRejection(async (req, res) => {

const additionalFields = await validateAndGetAdditionalFields(userFields)

await createUser({
...additionalFields,
username: userFields.username,
password: userFields.password,
})
await createUser(
{
username: userFields.username,
password: userFields.password,
},
additionalFields
)

return res.json({ success: true })
})

function ensureValidArgs(args: unknown): void {
ensureValidUsername(args);
ensurePasswordIsPresent(args);
ensureValidPassword(args);
ensureValidUsername(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createDefineAdditionalSignupFieldsFn } from '../types.js'

export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn<"username" | "password">()
export const defineAdditionalSignupFields = createDefineAdditionalSignupFieldsFn()
19 changes: 9 additions & 10 deletions waspc/data/Generator/templates/server/src/auth/providers/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{{={= =}=}}
import type { Router, Request } from 'express'
import type { User } from '../../entities'
import type { {= userEntityUpper =} } from '../../entities'
import type { Expand } from '../../universal/types'

type UserEntity = {= userEntityUpper =}

export type ProviderConfig = {
// Unique provider identifier, used as part of URL paths
id: string;
@@ -20,16 +23,12 @@ export type InitData = {

export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }

export function createDefineAdditionalSignupFieldsFn<
// Wasp already includes these fields in the signup process
ExistingFields extends keyof User,
PossibleAdditionalFields = Expand<
Partial<Omit<User, ExistingFields>>
>
>() {
export type PossibleAdditionalSignupFields = Expand<Partial<UserEntity>>

export function createDefineAdditionalSignupFieldsFn() {
return function defineFields(config: {
[key in keyof PossibleAdditionalFields]: FieldGetter<
PossibleAdditionalFields[key]
[key in keyof PossibleAdditionalSignupFields]: FieldGetter<
PossibleAdditionalSignupFields[key]
>
}) {
return config
33 changes: 22 additions & 11 deletions waspc/data/Generator/templates/server/src/auth/utils.ts
Original file line number Diff line number Diff line change
@@ -5,21 +5,21 @@ import HttpError from '../core/HttpError.js'
import prisma from '../dbClient.js'
import { isPrismaError, prismaErrorToHttpError, sleep } from '../utils.js'
import { type {= userEntityUpper =} } from '../entities/index.js'
import { type Prisma } from '@prisma/client';
import { type Prisma, type {= authEntityUpper =} } from '@prisma/client';

import { throwValidationError } from './validation.js'

{=# additionalSignupFields.isDefined =}
{=& additionalSignupFields.importStatement =}
{=/ additionalSignupFields.isDefined =}

import { createDefineAdditionalSignupFieldsFn, type PossibleAdditionalSignupFields } from './providers/types.js'
{=# additionalSignupFields.isDefined =}
const _waspAdditionalSignupFieldsConfig = {= additionalSignupFields.importIdentifier =}
{=/ additionalSignupFields.isDefined =}
{=^ additionalSignupFields.isDefined =}
import { createDefineAdditionalSignupFieldsFn } from './providers/types.js'
const _waspAdditionalSignupFieldsConfig = {} as ReturnType<
ReturnType<typeof createDefineAdditionalSignupFieldsFn<never>>
ReturnType<typeof createDefineAdditionalSignupFieldsFn>
>
{=/ additionalSignupFields.isDefined =}

@@ -34,28 +34,39 @@ export const authConfig = {
successRedirectPath: "{= successRedirectPath =}",
}

export async function findUserBy(where: Prisma.{= userEntityUpper =}WhereUniqueInput): Promise<{= userEntityUpper =}> {
return prisma.{= userEntityLower =}.findUnique({ where });
export async function findAuthWithUserBy(where: Prisma.{= authEntityUpper =}WhereUniqueInput) {
return prisma.{= authEntityLower =}.findUnique({ where, include: { {= userFieldOnAuthEntityName =}: true }});
}

export async function createUser(data: Prisma.{= userEntityUpper =}CreateInput): Promise<{= userEntityUpper =}> {
export async function createUser(data: Prisma.{= authEntityUpper =}CreateInput, additionalFields: PossibleAdditionalSignupFields) {
try {
return await prisma.{= userEntityLower =}.create({ data })
return await prisma.{= authEntityLower =}.create({
data: {
...data,
{= userFieldOnAuthEntityName =}: {
create: {
...additionalFields,
}
}
}
})
} catch (e) {
rethrowPossiblePrismaError(e);
}
}

export async function deleteUser(user: {= userEntityUpper =}): Promise<{= userEntityUpper =}> {
export async function deleteUser(auth: {= authEntityUpper =}) {
try {
return await prisma.{= userEntityLower =}.delete({ where: { id: user.id } })
return await prisma.{= authEntityLower =}.delete({ where: { id: auth.id } })
} catch (e) {
rethrowPossiblePrismaError(e);
}
}

export async function createAuthToken(user: {= userEntityUpper =}): Promise<string> {
return sign(user.id);
export async function createAuthToken(
auth: {= authEntityUpper =} & { {= userFieldOnAuthEntityName =}: {= userEntityUpper =} }
): Promise<string> {
return sign(auth.{= userFieldOnAuthEntityName =}.id);
}

export async function verifyToken(token: string): Promise<{ id: any }> {
22 changes: 18 additions & 4 deletions waspc/data/Generator/templates/server/src/core/auth.js
Original file line number Diff line number Diff line change
@@ -48,7 +48,13 @@ export async function getUserFromToken(token) {
}
}

const user = await prisma.{= userEntityLower =}.findUnique({ where: { id: userIdFromToken } })
const user = await prisma.{= userEntityLower =}
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
.findUnique({
where: { id: userIdFromToken },
include: {
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
{= authFieldOnUserEntityName =}: true
}
})
if (!user) {
throwInvalidCredentialsError()
}
@@ -57,9 +63,17 @@ export async function getUserFromToken(token) {
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
const { password, ...userView } = user

return userView
const {
{= authFieldOnUserEntityName =}: { password, ...{= authFieldOnUserEntityName =}Fields },
...fields
} = user

return {
...fields,
auth: {
...{= authFieldOnUserEntityName =}Fields
}
}
}

const SP = new SecurePassword()
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { PASSWORD_FIELD } from '../../auth/validation.js'
// Make sure password is always hashed before storing to the database.
const registerPasswordHashing = (prismaClient) => {
prismaClient.$use(async (params, next) => {
if (params.model === '{= userEntityUpper =}') {
if (params.model === '{= authEntityUpper =}') {
if (['create', 'update', 'updateMany'].includes(params.action)) {
if (params.args.data.hasOwnProperty(PASSWORD_FIELD)) {
params.args.data[PASSWORD_FIELD] = await hashPassword(params.args.data[PASSWORD_FIELD])
5 changes: 5 additions & 0 deletions waspc/examples/auth-model-experiment/main.wasp
Original file line number Diff line number Diff line change
@@ -21,6 +21,11 @@ entity User {=psl
id String @id @default(uuid())
psl=}

route LoginRoute { path: "/login", to: Login }
page Login {
component: import { Login } from "@client/auth.jsx"
}

route SignupRoute { path: "/signup", to: Signup }
page Signup {
component: import { Signup } from "@client/auth.jsx"
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"authId" TEXT
"id" TEXT NOT NULL PRIMARY KEY
);

-- CreateTable
@@ -10,7 +9,7 @@ CREATE TABLE "Auth" (
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"userId" TEXT,
CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
65 changes: 37 additions & 28 deletions waspc/examples/auth-model-experiment/src/client/MainPage.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
import waspLogo from './waspLogo.png'
import './Main.css'
import waspLogo from "./waspLogo.png";
import "./Main.css";

import logout from "@wasp/auth/logout";
import useAuth from "@wasp/auth/useAuth";

import { Link } from "@wasp/router";

const MainPage = () => {
const { data: user } = useAuth();
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>

<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>src/client/MainPage.jsx</code> to edit it.
</h3>
<h2 className="welcome-title">
{" "}
Welcome to Wasp - you just started a new app!{" "}
</h2>

<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorial/create"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
{user && (
<div className="user-info">
<p>
{" "}
You are logged in as <strong>{user.auth.username}</strong>!{" "}
</p>
<p>
{" "}
Your user id is <strong>{user.id}</strong>.{" "}
</p>
</div>
)}

{user && (
<button className="button" onClick={logout}>
Logout
</button>
)}

<Link to="/login" className="button">
Login
</Link>
</main>
</div>
)
}
export default MainPage
);
};
export default MainPage;
5 changes: 5 additions & 0 deletions waspc/examples/auth-model-experiment/src/client/auth.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { SignupForm } from "@wasp/auth/forms/Signup";
import { LoginForm } from "@wasp/auth/forms/Login";

export function Signup() {
return <SignupForm />;
}

export function Login() {
return <LoginForm />;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import { defineConfig } from "vite";

export default defineConfig({
server: {
open: true,
open: false,
},
})
});
61 changes: 11 additions & 50 deletions waspc/src/Wasp/Generator/DbGenerator.hs
Original file line number Diff line number Diff line change
@@ -8,12 +8,10 @@ module Wasp.Generator.DbGenerator
where

import Data.Aeson (object, (.=))
import Data.Maybe (fromJust, fromMaybe, maybeToList)
import Data.List (find)
import Data.Maybe (fromMaybe, maybeToList)
import Data.Text (Text, pack)
import qualified Data.Text as T
import NeatInterpolation (trimming)
import StrongPath (Abs, Dir, File, Path', Rel, (</>))
import Wasp.Analyzer.StdTypeDefinitions.Entity (parsePslBody)
import Wasp.AppSpec (AppSpec, getEntities)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
@@ -22,6 +20,7 @@ import qualified Wasp.AppSpec.App.Db as AS.Db
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.Common (ProjectRootDir)
import qualified Wasp.Generator.DbGenerator.Auth as DbAuth
import Wasp.Generator.DbGenerator.Common
( DbSchemaChecksumFile,
DbSchemaChecksumOnLastDbConcurrenceFile,
@@ -46,7 +45,6 @@ import Wasp.Generator.Monad
)
import Wasp.Project.Db (databaseUrlEnvVarName)
import qualified Wasp.Psl.Ast.Model as Psl.Ast.Model
import qualified Wasp.Psl.Ast.Model as Psl.Model
import qualified Wasp.Psl.Generator.Extensions as Psl.Generator.Extensions
import qualified Wasp.Psl.Generator.Model as Psl.Generator.Model
import Wasp.Util (checksumFromFilePath, hexToString, ifM, (<:>))
@@ -68,15 +66,11 @@ genPrismaSchema spec = do
then logAndThrowGeneratorError $ GenericGeneratorError "SQLite (a default database) is not supported in production. To build your Wasp app for production, switch to a different database. Switching to PostgreSQL: https://wasp-lang.dev/docs/data-model/backends#migrating-from-sqlite-to-postgresql ."
else return ("sqlite", "\"file:./dev.db\"")

let entities = injectAuthIntoUserEntity $ AS.getDecls @AS.Entity.Entity spec

authEntities <- makeAuthEntity "String" maybeUserEntityName

let modelSchemas = map entityToPslModelSchema (entities ++ authEntities)
entities <- DbAuth.injectAuth maybeUserEntity userDefinedEntities

let templateData =
object
[ "modelSchemas" .= modelSchemas,
[ "modelSchemas" .= map entityToPslModelSchema entities,
"datasourceProvider" .= datasourceProvider,
"datasourceUrl" .= datasourceUrl,
"prismaClientOutputDir" .= makeEnvVarField Wasp.Generator.DbGenerator.Common.prismaClientOutputDirEnvVar,
@@ -92,46 +86,13 @@ genPrismaSchema spec = do
prismaPreviewFeatures = show <$> (AS.Db.clientPreviewFeatures =<< AS.Db.prisma =<< AS.App.db (snd $ getApp spec))
dbExtensions = Psl.Generator.Extensions.showDbExtensions <$> (AS.Db.dbExtensions =<< AS.Db.prisma =<< AS.App.db (snd $ getApp spec))

maybeUserEntityName = AS.refName . AS.Auth.userEntity <$> maybeAuth
maybeAuth = AS.App.auth $ snd $ getApp spec

makeAuthEntity :: String -> Maybe String -> Generator [(String, AS.Entity.Entity)]
makeAuthEntity _ Nothing = return []
makeAuthEntity userEntityIdType (Just userEntityName) = case parsePslBody authEntityPslBody of
Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating Auth entity: " ++ show err
Right pslBody -> return [("Auth", AS.Entity.makeEntity pslBody)]
where
authEntityPslBody =
T.unpack
[trimming|
id String @id @default(uuid())
username String @unique
password String
userId ${userEntityIdTypeText}? @unique
user ${userEntityNameText}? @relation(fields: [userId], references: [id])
|]

userEntityIdTypeText = T.pack userEntityIdType
userEntityNameText = T.pack userEntityName
userDefinedEntities = getEntities spec

injectAuthIntoUserEntity :: [(String, AS.Entity.Entity)] -> [(String, AS.Entity.Entity)]
injectAuthIntoUserEntity entities =
case maybeUserEntityName of
Nothing -> entities
Just userEntityName ->
let userEntity = fromJust $ lookup userEntityName entities
userEntityWithAuthInjected = injectRelationToAuth userEntity
in (userEntityName, userEntityWithAuthInjected) : filter ((/= userEntityName) . fst) entities
where
injectRelationToAuth :: AS.Entity.Entity -> AS.Entity.Entity
injectRelationToAuth entity = AS.Entity.makeEntity newPslBody
where
(Psl.Model.Body oldPslElements) = AS.Entity.getPslModelBody entity
newPslElements =
[ Psl.Model.ElementField $ Psl.Model.Field "authId" Psl.Model.String [Psl.Model.Optional] [],
Psl.Model.ElementField $ Psl.Model.Field "auth" (Psl.Model.UserType "Auth") [Psl.Model.Optional] []
]
newPslBody = Psl.Model.Body $ oldPslElements ++ newPslElements
maybeUserEntity :: Maybe (String, AS.Entity.Entity)
infomiho marked this conversation as resolved.
Show resolved Hide resolved
maybeUserEntity = do
auth <- AS.App.auth $ snd $ getApp spec
let userEntityName = AS.refName . AS.Auth.userEntity $ auth
find ((== userEntityName) . fst) userDefinedEntities

entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
entityToPslModelSchema (entityName, entity) =
78 changes: 78 additions & 0 deletions waspc/src/Wasp/Generator/DbGenerator/Auth.hs
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
module Wasp.Generator.DbGenerator.Auth where
Martinsos marked this conversation as resolved.
Show resolved Hide resolved

import Data.Function ((&))
import Data.Maybe
import qualified Data.Text as T
import NeatInterpolation (trimming)
import Wasp.Analyzer.StdTypeDefinitions.Entity (parsePslBody)
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.Generator.Monad
( Generator,
GeneratorError (GenericGeneratorError),
logAndThrowGeneratorError,
)
import qualified Wasp.Psl.Ast.Model as Psl.Model
import qualified Wasp.Psl.Ast.Model as Psl.Model.Field

authEntityName :: String
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
authEntityName = "Auth"

userFieldOnAuthEntityName :: String
userFieldOnAuthEntityName = "user"

authFieldOnUserEntityName :: String
authFieldOnUserEntityName = "auth"

injectAuth :: Maybe (String, AS.Entity.Entity) -> [(String, AS.Entity.Entity)] -> Generator [(String, AS.Entity.Entity)]
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
injectAuth Nothing entities = return entities
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
injectAuth (Just (userEntityName, userEntity)) entities = do
userEntityIdType <- getUserEntityId userEntity
authEntity <- makeAuthEntity userEntityIdType userEntityName
return $ injectAuthIntoUserEntity userEntityName $ entities ++ authEntity

getUserEntityId :: AS.Entity.Entity -> Generator String
getUserEntityId entity =
show . Psl.Model.Field._type <$> AS.Entity.getIdField entity
& ( \case
Nothing -> logAndThrowGeneratorError $ GenericGeneratorError "User entity does not have an id field."
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
Just idType -> return idType
)

makeAuthEntity :: String -> String -> Generator [(String, AS.Entity.Entity)]
makeAuthEntity userEntityIdType userEntityName = case parsePslBody authEntityPslBody of
Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating Auth entity: " ++ show err
Right pslBody -> return [(authEntityName, AS.Entity.makeEntity pslBody)]
where
authEntityPslBody =
T.unpack
[trimming|
id String @id @default(uuid())
username String @unique
password String
userId ${userEntityIdTypeText}? @unique
${userFieldOnAuthEntityNameText} ${userEntityNameText}? @relation(fields: [userId], references: [id], onDelete: Cascade)
|]

userEntityIdTypeText = T.pack userEntityIdType
userEntityNameText = T.pack userEntityName
userFieldOnAuthEntityNameText = T.pack userFieldOnAuthEntityName

injectAuthIntoUserEntity :: String -> [(String, AS.Entity.Entity)] -> [(String, AS.Entity.Entity)]
injectAuthIntoUserEntity userEntityName entities =
let userEntity = fromJust $ lookup userEntityName entities
userEntityWithAuthInjected = injectRelationToAuth userEntity
in (userEntityName, userEntityWithAuthInjected) : filter ((/= userEntityName) . fst) entities
where
injectRelationToAuth :: AS.Entity.Entity -> AS.Entity.Entity
injectRelationToAuth entity = AS.Entity.makeEntity newPslBody
where
(Psl.Model.Body existingPsl) = AS.Entity.getPslModelBody entity
relationToAuthEntity =
[ Psl.Model.ElementField $
Psl.Model.Field
authFieldOnUserEntityName
(Psl.Model.UserType authEntityName)
[Psl.Model.Optional]
[]
]
newPslBody = Psl.Model.Body $ existingPsl ++ relationToAuthEntity
3 changes: 3 additions & 0 deletions waspc/src/Wasp/Generator/ServerGenerator.hs
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ import Wasp.Generator.Common
makeJsonWithEntityData,
prismaVersion,
)
import qualified Wasp.Generator.DbGenerator.Auth as DbAuth
import Wasp.Generator.ExternalCodeGenerator (genExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft, createTextFileDraft)
import Wasp.Generator.Monad (Generator)
@@ -316,6 +317,8 @@ genTypesAndEntitiesDirs spec =
[ "entities" .= allEntities,
"isAuthEnabled" .= isJust maybeUserEntityName,
"userEntityName" .= userEntityName,
"authEntityName" .= DbAuth.authEntityName,
"authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName,
"userFieldName" .= toLowerFirst userEntityName
]
)
36 changes: 19 additions & 17 deletions waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs
Original file line number Diff line number Diff line change
@@ -19,11 +19,12 @@ import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (doesUserEntityContainField, getApp)
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.AuthProviders (emailAuthProvider, gitHubAuthProvider, googleAuthProvider, localAuthProvider)
import qualified Wasp.Generator.AuthProviders.Email as EmailProvider
import qualified Wasp.Generator.AuthProviders.Local as LocalProvider
import qualified Wasp.Generator.AuthProviders.OAuth as OAuthProvider
import qualified Wasp.Generator.DbGenerator.Auth as DbAuth
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.ServerGenerator.Auth.EmailAuthG (genEmailAuth)
@@ -39,12 +40,12 @@ genAuth spec = case maybeAuth of
Just auth ->
sequence
[ genCoreAuth auth,
genAuthMiddleware spec auth,
genAuthMiddleware,
genAuthRoutesIndex auth,
genMeRoute auth,
genUtils auth,
genProvidersIndex auth,
genFileCopy [relfile|auth/providers/types.ts|],
genProvidersTypes auth,
genFileCopy [relfile|auth/validation.ts|]
]
<++> genIndexTs auth
@@ -68,11 +69,12 @@ genCoreAuth auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmpl
let userEntityName = AS.refName $ AS.Auth.userEntity auth
in object
[ "userEntityUpper" .= (userEntityName :: String),
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String)
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
"authFieldOnUserEntityName" .= (DbAuth.authFieldOnUserEntityName :: String)
]

genAuthMiddleware :: AS.AppSpec -> AS.Auth.Auth -> Generator FileDraft
genAuthMiddleware spec auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
genAuthMiddleware :: Generator FileDraft
genAuthMiddleware = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where
-- TODO(martin): In prismaMiddleware.js, we assume that 'username' and 'password' are defined in user entity.
-- This was promised to us by AppSpec, which has validation checks for this.
@@ -84,17 +86,7 @@ genAuthMiddleware spec auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile
tmplFile = C.asTmplFile $ [reldir|src|] </> authMiddlewareRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile authMiddlewareRelToSrc

tmplData =
let userEntityName = AS.refName $ AS.Auth.userEntity auth
isPasswordOnUserEntity = doesUserEntityContainField spec "password" == Just True
isUsernameOnUserEntity = doesUserEntityContainField spec "username" == Just True
in object
[ "userEntityUpper" .= userEntityName,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"isPasswordOnUserEntity" .= isPasswordOnUserEntity,
"isUsernameOnUserEntity" .= isUsernameOnUserEntity,
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
]
tmplData = object ["authEntityUpper" .= (DbAuth.authEntityName :: String)]

genAuthRoutesIndex :: AS.Auth.Auth -> Generator FileDraft
genAuthRoutesIndex auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
@@ -126,6 +118,9 @@ genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplDat
object
[ "userEntityUpper" .= (userEntityName :: String),
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
"authEntityUpper" .= (DbAuth.authEntityName :: String),
"authEntityLower" .= (Util.toLowerFirst DbAuth.authEntityName :: String),
"userFieldOnAuthEntityName" .= (DbAuth.userFieldOnAuthEntityName :: String),
"failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo auth,
"successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth,
"additionalSignupFields" .= extImportToImportJson [reldirP|../|] additionalSignupFields
@@ -166,3 +161,10 @@ genProvidersIndex auth = return $ C.mkTmplFdWithData [relfile|src/auth/providers
[LocalProvider.providerId localAuthProvider | AS.Auth.isUsernameAndPasswordAuthEnabled auth],
[EmailProvider.providerId emailAuthProvider | AS.Auth.isEmailAuthEnabled auth]
]

genProvidersTypes :: AS.Auth.Auth -> Generator FileDraft
genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|src/auth/providers/types.ts|] (Just tmplData)
where
userEntityName = AS.refName $ AS.Auth.userEntity auth

tmplData = object ["userEntityUpper" .= (userEntityName :: String)]
1 change: 1 addition & 0 deletions waspc/waspc.cabal
Original file line number Diff line number Diff line change
@@ -242,6 +242,7 @@ library
Wasp.Generator.ConfigFile
Wasp.Generator.ConfigFileGenerator
Wasp.Generator.DbGenerator
Wasp.Generator.DbGenerator.Auth
Wasp.Generator.DbGenerator.Common
Wasp.Generator.DbGenerator.Jobs
Wasp.Generator.DbGenerator.Operations