From a35040e3515a4fcb2c209eb29761b4b29ebac1df Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Fri, 19 Jan 2024 18:28:42 +0100 Subject: [PATCH] Apply latest auth changes to the prototype (#1646) --- .../20240119141512_add_session/migration.sql | 13 -- .../migrations/migration_lock.toml | 3 - .../templates/sdk/wasp/api/events.ts | 6 +- .../Generator/templates/sdk/wasp/api/index.ts | 47 +++--- .../templates/sdk/wasp/auth/helpers/user.ts | 6 +- .../Generator/templates/sdk/wasp/auth/jwt.ts | 12 ++ .../templates/sdk/wasp/auth/login.ts | 2 +- .../templates/sdk/wasp/auth/logout.ts | 18 ++- .../templates/sdk/wasp/auth/lucia.ts | 55 +++++++ .../templates/sdk/wasp/auth/password.ts | 15 ++ .../sdk/wasp/auth/providers/types.ts | 14 +- .../templates/sdk/wasp/auth/session.ts | 107 +++++++++++++ .../templates/sdk/wasp/auth/types.ts | 2 +- .../Generator/templates/sdk/wasp/auth/user.ts | 4 +- .../templates/sdk/wasp/auth/utils.ts | 46 ++++-- .../Generator/templates/sdk/wasp/core/auth.js | 148 +++--------------- .../templates/sdk/wasp/server/_types/index.ts | 6 +- .../templates/sdk/wasp/server/utils.ts | 3 +- .../20230816092617_migracijone/migration.sql | 18 --- .../20240119151915_init}/migration.sql | 14 ++ 20 files changed, 314 insertions(+), 225 deletions(-) delete mode 100644 examples/todo-typescript/migrations/20240119141512_add_session/migration.sql delete mode 100644 examples/todo-typescript/migrations/migration_lock.toml create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/password.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/auth/session.ts delete mode 100644 waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql rename {examples/todo-typescript/migrations/20231214130914_new_auth => waspc/examples/todo-typescript/migrations/20240119151915_init}/migration.sql (72%) diff --git a/examples/todo-typescript/migrations/20240119141512_add_session/migration.sql b/examples/todo-typescript/migrations/20240119141512_add_session/migration.sql deleted file mode 100644 index ae20ab49b..000000000 --- a/examples/todo-typescript/migrations/20240119141512_add_session/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL PRIMARY KEY, - "expiresAt" DATETIME NOT NULL, - "userId" TEXT NOT NULL, - CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); - --- CreateIndex -CREATE INDEX "Session_userId_idx" ON "Session"("userId"); diff --git a/examples/todo-typescript/migrations/migration_lock.toml b/examples/todo-typescript/migrations/migration_lock.toml deleted file mode 100644 index e5e5c4705..000000000 --- a/examples/todo-typescript/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file diff --git a/waspc/data/Generator/templates/sdk/wasp/api/events.ts b/waspc/data/Generator/templates/sdk/wasp/api/events.ts index 9a59b366d..a72e48dda 100644 --- a/waspc/data/Generator/templates/sdk/wasp/api/events.ts +++ b/waspc/data/Generator/templates/sdk/wasp/api/events.ts @@ -3,9 +3,9 @@ import mitt, { Emitter } from 'mitt'; type ApiEvents = { // key: Event name // type: Event payload type - 'authToken.set': void; - 'authToken.clear': void; + 'sessionId.set': void; + 'sessionId.clear': void; }; -// Used to allow API clients to register for auth token change events. +// Used to allow API clients to register for auth session ID change events. export const apiEventsEmitter: Emitter = mitt(); diff --git a/waspc/data/Generator/templates/sdk/wasp/api/index.ts b/waspc/data/Generator/templates/sdk/wasp/api/index.ts index 9aad1ead5..8b22dd7eb 100644 --- a/waspc/data/Generator/templates/sdk/wasp/api/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/api/index.ts @@ -8,59 +8,60 @@ const api = axios.create({ baseURL: config.apiUrl, }) -const WASP_APP_AUTH_TOKEN_NAME = 'authToken' +const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId' -let authToken = storage.get(WASP_APP_AUTH_TOKEN_NAME) as string | undefined +let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined -export function setAuthToken(token: string): void { - authToken = token - storage.set(WASP_APP_AUTH_TOKEN_NAME, token) - apiEventsEmitter.emit('authToken.set') +export function setSessionId(sessionId: string): void { + waspAppAuthSessionId = sessionId + storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId) + apiEventsEmitter.emit('sessionId.set') } -export function getAuthToken(): string | undefined { - return authToken +export function getSessionId(): string | undefined { + return waspAppAuthSessionId } -export function clearAuthToken(): void { - authToken = undefined - storage.remove(WASP_APP_AUTH_TOKEN_NAME) - apiEventsEmitter.emit('authToken.clear') +export function clearSessionId(): void { + waspAppAuthSessionId = undefined + storage.remove(WASP_APP_AUTH_SESSION_ID_NAME) + apiEventsEmitter.emit('sessionId.clear') } export function removeLocalUserData(): void { - authToken = undefined + waspAppAuthSessionId = undefined storage.clear() - apiEventsEmitter.emit('authToken.clear') + apiEventsEmitter.emit('sessionId.clear') } api.interceptors.request.use((request) => { - if (authToken) { - request.headers['Authorization'] = `Bearer ${authToken}` + const sessionId = getSessionId() + if (sessionId) { + request.headers['Authorization'] = `Bearer ${sessionId}` } return request }) api.interceptors.response.use(undefined, (error) => { if (error.response?.status === 401) { - clearAuthToken() + clearSessionId() } return Promise.reject(error) }) // This handler will run on other tabs (not the active one calling API functions), -// and will ensure they know about auth token changes. +// and will ensure they know about auth session ID changes. // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event // "Note: This won't work on the same page that is making the changes — it is really a way // for other pages on the domain using the storage to sync any changes that are made." window.addEventListener('storage', (event) => { - if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_TOKEN_NAME)) { + if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) { if (!!event.newValue) { - authToken = event.newValue - apiEventsEmitter.emit('authToken.set') + waspAppAuthSessionId = event.newValue + apiEventsEmitter.emit('sessionId.set') } else { - authToken = undefined - apiEventsEmitter.emit('authToken.clear') + waspAppAuthSessionId = undefined + apiEventsEmitter.emit('sessionId.clear') } } }) diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts index c3e6a4072..498f2588a 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/helpers/user.ts @@ -1,8 +1,8 @@ -import { setAuthToken } from 'wasp/api' +import { setSessionId } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' -export async function initSession(token: string): Promise { - setAuthToken(token) +export async function initSession(sessionId: string): Promise { + setSessionId(sessionId) // We need to invalidate queries after login in order to get the correct user // data in the React components (using `useAuth`). // Redirects after login won't work properly without this. diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts b/waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts new file mode 100644 index 000000000..06c0f10d3 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts @@ -0,0 +1,12 @@ +import jwt from 'jsonwebtoken' +import util from 'util' + +import config from 'wasp/core/config' + +const jwtSign = util.promisify(jwt.sign) +const jwtVerify = util.promisify(jwt.verify) + +const JWT_SECRET = config.auth.jwtSecret + +export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) +export const verify = (token) => jwtVerify(token, JWT_SECRET) diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/login.ts b/waspc/data/Generator/templates/sdk/wasp/auth/login.ts index 487b45b98..2b4ec4b9f 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/login.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/login.ts @@ -6,7 +6,7 @@ export default async function login(username: string, password: string): Promise const args = { username, password } const response = await api.post('/auth/username/login', args) - await initSession(response.data.token) + await initSession(response.data.sessionId) } catch (error) { handleApiError(error) } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts b/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts index 340e9dec9..cc41b6989 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/logout.ts @@ -1,9 +1,17 @@ -import { removeLocalUserData } from 'wasp/api' +import api, { removeLocalUserData } from 'wasp/api' import { invalidateAndRemoveQueries } from 'wasp/operations/resources' export default async function logout(): Promise { - removeLocalUserData() - // TODO(filip): We are currently invalidating and removing all the queries, but - // we should remove only the non-public, user-dependent ones. - await invalidateAndRemoveQueries() + try { + await api.post('/auth/logout') + } finally { + // Even if the logout request fails, we still want to remove the local user data + // in case the logout failed because of a network error and the user walked away + // from the computer. + removeLocalUserData() + + // TODO(filip): We are currently invalidating and removing all the queries, but + // we should remove only the non-public, user-dependent ones. + await invalidateAndRemoveQueries() + } } diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts b/waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts new file mode 100644 index 000000000..690d090d4 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts @@ -0,0 +1,55 @@ +import { Lucia } from "lucia"; +import { PrismaAdapter } from "@lucia-auth/adapter-prisma"; +import prisma from '../server/dbClient.js' +import config from 'wasp/core/config' +import { type User } from "../entities/index.js" + +const prismaAdapter = new PrismaAdapter( + // Using `as any` here since Lucia's model types are not compatible with Prisma 4 + // model types. This is a temporary workaround until we migrate to Prisma 5. + // This **works** in runtime, but Typescript complains about it. + prisma.session as any, + prisma.auth as any +); + +/** + * We are using Lucia for session management. + * + * Some details: + * 1. We are using the Prisma adapter for Lucia. + * 2. We are not using cookies for session management. Instead, we are using + * the Authorization header to send the session token. + * 3. Our `Session` entity is connected to the `Auth` entity. + * 4. We are exposing the `userId` field from the `Auth` entity to + * make fetching the User easier. + */ +export const auth = new Lucia<{}, { + userId: User['id'] +}>(prismaAdapter, { + // Since we are not using cookies, we don't need to set any cookie options. + // But in the future, if we decide to use cookies, we can set them here. + + // sessionCookie: { + // name: "session", + // expires: true, + // attributes: { + // secure: !config.isDevelopment, + // sameSite: "lax", + // }, + // }, + getUserAttributes({ userId }) { + return { + userId, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof auth; + DatabaseSessionAttributes: {}; + DatabaseUserAttributes: { + userId: User['id'] + }; + } +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/password.ts b/waspc/data/Generator/templates/sdk/wasp/auth/password.ts new file mode 100644 index 000000000..a359892b5 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/password.ts @@ -0,0 +1,15 @@ +import SecurePassword from 'secure-password' + +const SP = new SecurePassword() + +export const hashPassword = async (password: string): Promise => { + const hashedPwdBuffer = await SP.hash(Buffer.from(password)) + return hashedPwdBuffer.toString("base64") +} + +export const verifyPassword = async (hashedPassword: string, password: string): Promise => { + const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + if (result !== SecurePassword.VALID) { + throw new Error('Invalid password.') + } +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts index 5bbc99ca8..76e111485 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/providers/types.ts @@ -23,16 +23,18 @@ export type InitData = { export type RequestWithWasp = Request & { wasp?: { [key: string]: any } } -export type PossibleAdditionalSignupFields = Expand> +export type PossibleUserFields = Expand> -export function defineAdditionalSignupFields(config: { - [key in keyof PossibleAdditionalSignupFields]: FieldGetter< - PossibleAdditionalSignupFields[key] +export type UserSignupFields = { + [key in keyof PossibleUserFields]: FieldGetter< + PossibleUserFields[key] > -}) { - return config } type FieldGetter = ( data: { [key: string]: unknown } ) => Promise | T | undefined + +export function defineUserSignupFields(fields: UserSignupFields) { + return fields +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/session.ts b/waspc/data/Generator/templates/sdk/wasp/auth/session.ts new file mode 100644 index 000000000..ed9154120 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/auth/session.ts @@ -0,0 +1,107 @@ +import { Request as ExpressRequest } from "express"; + +import { type User } from "../entities/index.js" +import { type SanitizedUser } from '../server/_types/index.js' + +import { auth } from "./lucia.js"; +import type { Session } from "lucia"; +import { + throwInvalidCredentialsError, + deserializeAndSanitizeProviderData, +} from "./utils.js"; + +import prisma from '../server/dbClient.js' + +// Creates a new session for the `authId` in the database +export async function createSession(authId: string): Promise { + return auth.createSession(authId, {}); +} + +export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const authorizationHeader = req.headers["authorization"]; + + if (typeof authorizationHeader !== "string") { + return { + user: null, + session: null, + }; + } + + const sessionId = auth.readBearerToken(authorizationHeader); + if (!sessionId) { + return { + user: null, + session: null, + }; + } + + return getSessionAndUserFromSessionId(sessionId); +} + +export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{ + user: SanitizedUser | null, + session: Session | null, +}> { + const { session, user: authEntity } = await auth.validateSession(sessionId); + + if (!session || !authEntity) { + return { + user: null, + session: null, + }; + } + + return { + session, + user: await getUser(authEntity.userId) + } +} + +async function getUser(userId: User['id']): Promise { + const user = await prisma.user + .findUnique({ + where: { id: userId }, + include: { + auth: { + include: { + identities: true + } + } + } + }) + + if (!user) { + throwInvalidCredentialsError() + } + + // TODO: This logic must match the type in _types/index.ts (if we remove the + // 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 deserializedIdentities = user.auth.identities.map((identity) => { + const deserializedProviderData = deserializeAndSanitizeProviderData( + identity.providerData, + { + shouldRemovePasswordField: true, + } + ) + return { + ...identity, + providerData: deserializedProviderData, + } + }) + return { + ...user, + auth: { + ...user.auth, + identities: deserializedIdentities, + }, + } +} + +export function invalidateSession(sessionId: string): Promise { + return auth.invalidateSession(sessionId); +} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts index 9240b4e4b..f9f079a57 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/' +export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/' diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts index 5799c71ea..aa0da2482 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts @@ -2,7 +2,7 @@ // We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts // If you are changing the logic here, make sure to change it there as well. -import type { User, ProviderName, DeserializedAuthEntity } from './types' +import type { User, ProviderName, DeserializedAuthIdentity } from './types' export function getEmail(user: User): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; @@ -20,7 +20,7 @@ export function getFirstProviderUserId(user?: User): string | null { return user.auth.identities[0].providerUserId ?? null; } -export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthEntity | undefined { +export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts index 7a180abdc..603e9a4b1 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts @@ -1,4 +1,5 @@ -import { hashPassword, sign, verify } from 'wasp/core/auth' +import { hashPassword } from './password.js' +import { verify } from './jwt.js' import AuthError from '../core/AuthError.js' import HttpError from '../core/HttpError.js' import prisma from '../server/dbClient.js' @@ -12,9 +13,7 @@ import { Prisma } from '@prisma/client'; import { throwValidationError } from './validation.js' - -import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js' -const _waspAdditionalSignupFieldsConfig = {} as ReturnType +import { type UserSignupFields, type PossibleUserFields } from './providers/types.js' export type EmailProviderData = { hashedPassword: string; @@ -127,8 +126,10 @@ export async function findAuthWithUserBy( export async function createUser( providerId: ProviderId, serializedProviderData?: string, - userFields?: PossibleAdditionalSignupFields, -): Promise { + userFields?: PossibleUserFields, +): Promise { return prisma.user.create({ data: { // Using any here to prevent type errors when userFields are not @@ -145,7 +146,12 @@ export async function createUser( }, } }, - } + }, + // We need to include the Auth entity here because we need `authId` + // to be able to create a session. + include: { + auth: true, + }, }) } @@ -155,12 +161,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe } } }) } -export async function createAuthToken( - userId: User['id'] -): Promise { - return sign(userId); -} - export async function verifyToken(token: string): Promise { return verify(token); } @@ -224,15 +224,23 @@ export function rethrowPossibleAuthError(e: unknown): void { throw e } -export async function validateAndGetAdditionalFields(data: { - [key: string]: unknown -}): Promise> { +export async function validateAndGetUserFields( + data: { + [key: string]: unknown + }, + userSignupFields?: UserSignupFields, +): Promise> { const { password: _password, ...sanitizedData } = data; const result: Record = {}; - for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) { + + if (!userSignupFields) { + return result; + } + + for (const [field, getFieldValue] of Object.entries(userSignupFields)) { try { const value = await getFieldValue(sanitizedData) result[field] = value @@ -288,3 +296,7 @@ function providerDataHasPasswordField( ): providerData is { hashedPassword: string } { return 'hashedPassword' in providerData; } + +export function throwInvalidCredentialsError(message?: string): void { + throw new HttpError(401, 'Invalid credentials', { message }) +} diff --git a/waspc/data/Generator/templates/sdk/wasp/core/auth.js b/waspc/data/Generator/templates/sdk/wasp/core/auth.js index 75e77a7fb..6908bfb51 100644 --- a/waspc/data/Generator/templates/sdk/wasp/core/auth.js +++ b/waspc/data/Generator/templates/sdk/wasp/core/auth.js @@ -1,23 +1,22 @@ -import jwt from 'jsonwebtoken' -import SecurePassword from 'secure-password' -import util from 'util' import { randomInt } from 'node:crypto' -import prisma from '@server/dbClient.js' -import { handleRejection } from '../server/utils' -import HttpError from './HttpError.js' -import config from '../config.js' -import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils' - -const jwtSign = util.promisify(jwt.sign) -const jwtVerify = util.promisify(jwt.verify) - -const JWT_SECRET = config.auth.jwtSecret - -export const signData = (data, options) => jwtSign(data, JWT_SECRET, options) -export const sign = (id, options) => signData({ id }, options) -export const verify = (token) => jwtVerify(token, JWT_SECRET) +import prisma from '../server/dbClient.js' +import { handleRejection } from '../utils.js' +import { getSessionAndUserFromBearerToken } from 'wasp/auth/session' +import { throwInvalidCredentialsError } from 'wasp/auth/utils' +/** + * Auth middleware + * + * If the request includes an `Authorization` header it will try to authenticate the request, + * otherwise it will let the request through. + * + * - If authentication succeeds it sets `req.sessionId` and `req.user` + * - `req.user` is the user that made the request and it's used in + * all Wasp features that need to know the user that made the request. + * - `req.sessionId` is the ID of the session that authenticated the request. + * - If the request is not authenticated, it throws an error. + */ const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { @@ -27,119 +26,16 @@ const auth = handleRejection(async (req, res, next) => { return next() } - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.substring(7, authHeader.length) - req.user = await getUserFromToken(token) - } else { + const { session, user } = await getSessionAndUserFromBearerToken(req); + + if (!session || !user) { throwInvalidCredentialsError() } + req.sessionId = session.id + req.user = user + next() }) -export async function getUserFromToken(token) { - let userIdFromToken - try { - userIdFromToken = (await verify(token)).id - } catch (error) { - if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { - throwInvalidCredentialsError() - } else { - throw error - } - } - - const user = await prisma.user - .findUnique({ - where: { id: userIdFromToken }, - include: { - auth: { - include: { - identities: true - } - } - } - }) - if (!user) { - throwInvalidCredentialsError() - } - - // TODO: This logic must match the type in types/index.ts (if we remove the - // 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 - let sanitizedUser = { ...user } - sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => { - identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true }) - return identity - }); - return sanitizedUser -} - -const SP = new SecurePassword() - -export const hashPassword = async (password) => { - const hashedPwdBuffer = await SP.hash(Buffer.from(password)) - return hashedPwdBuffer.toString("base64") -} - -export const verifyPassword = async (hashedPassword, password) => { - const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) - if (result !== SecurePassword.VALID) { - throw new Error('Invalid password.') - } -} - -// Generates an unused username that looks similar to "quick-purple-sheep-91231". -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableDictionaryUsername() { - const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young'] - const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow'] - const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep'] - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Generates an unused username based on an array of username segments and a separator. -// It generates several options and ensures it picks one that is not currently in use. -export function generateAvailableUsername(usernameSegments, config) { - const separator = config?.separator || '-' - const baseUsername = usernameSegments.join(separator) - - const potentialUsernames = [] - for (let i = 0; i < 10; i++) { - const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}` - potentialUsernames.push(potentialUsername) - } - - return findAvailableUsername(potentialUsernames) -} - -// Checks the database for an unused username from an array provided and returns first. -async function findAvailableUsername(potentialUsernames) { - const users = await prisma.user.findMany({ - where: { - username: { in: potentialUsernames }, - } - }) - const takenUsernames = users.map(user => user.username) - const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username)) - - if (availableUsernames.length === 0) { - throw new Error('Unable to generate a unique username. Please contact Wasp.') - } - - return availableUsernames[0] -} - -export function throwInvalidCredentialsError(message) { - throw new HttpError(401, 'Invalid credentials', { message }) -} - export default auth diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts index ad1de55e0..4a2afbd4b 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts @@ -82,18 +82,18 @@ type Context = Expand<{ type ContextWithUser = Expand & { user?: SanitizedUser }> -// TODO: This type must match the logic in core/auth.js (if we remove the +// TODO: This type must match the logic in auth/session.js (if we remove the // 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 DeserializedAuthEntity = Expand & { +export type DeserializedAuthIdentity = Expand & { providerData: Omit | Omit | OAuthProviderData }> export type SanitizedUser = User & { auth: Auth & { - identities: DeserializedAuthEntity[] + identities: DeserializedAuthIdentity[] } | null } diff --git a/waspc/data/Generator/templates/sdk/wasp/server/utils.ts b/waspc/data/Generator/templates/sdk/wasp/server/utils.ts index a930149d0..b0744f312 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/utils.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/utils.ts @@ -8,7 +8,8 @@ import { fileURLToPath } from 'url' import { type SanitizedUser } from './_types/index.js' type RequestWithExtraFields = Request & { - user?: SanitizedUser + user?: SanitizedUser; + sessionId?: string; } /** diff --git a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql b/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql deleted file mode 100644 index 7b6262767..000000000 --- a/waspc/examples/todo-typescript/migrations/20230816092617_migracijone/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "Task" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "description" TEXT NOT NULL, - "isDone" BOOLEAN NOT NULL DEFAULT false, - "userId" INTEGER, - CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); diff --git a/examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql b/waspc/examples/todo-typescript/migrations/20240119151915_init/migration.sql similarity index 72% rename from examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql rename to waspc/examples/todo-typescript/migrations/20240119151915_init/migration.sql index 0ea8e16da..919941fb1 100644 --- a/examples/todo-typescript/migrations/20231214130914_new_auth/migration.sql +++ b/waspc/examples/todo-typescript/migrations/20240119151915_init/migration.sql @@ -30,5 +30,19 @@ CREATE TABLE "AuthIdentity" ( CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL PRIMARY KEY, + "expiresAt" DATETIME NOT NULL, + "userId" TEXT NOT NULL, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + -- CreateIndex CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId");