mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 02:52:22 +03:00
Integrate Lucia Auth in Wasp (#1625)
This commit is contained in:
parent
ffe2509cb4
commit
73c37b7d6d
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -5,7 +5,7 @@ import { initSession } from '../../helpers/user';
|
||||
export async function login(data: { email: string; password: string }): Promise<void> {
|
||||
try {
|
||||
const response = await api.post('{= loginPath =}', data);
|
||||
await initSession(response.data.token);
|
||||
await initSession(response.data.sessionId);
|
||||
} catch (e) {
|
||||
handleApiError(e);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { setAuthToken } from '../../api'
|
||||
import { setSessionId } from '../../api'
|
||||
import { invalidateAndRemoveQueries } from '../../operations/resources'
|
||||
|
||||
export async function initSession(token: string): Promise<void> {
|
||||
setAuthToken(token)
|
||||
export async function initSession(sessionId: string): Promise<void> {
|
||||
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.
|
||||
|
@ -7,7 +7,7 @@ export default async function login(username: string, password: string): Promise
|
||||
const args = { username, password }
|
||||
const response = await api.post('{= loginPath =}', args)
|
||||
|
||||
await initSession(response.data.token)
|
||||
await initSession(response.data.sessionId)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { removeLocalUserData } from '../api'
|
||||
import api, { removeLocalUserData } from '../api'
|
||||
import { invalidateAndRemoveQueries } from '../operations/resources'
|
||||
|
||||
export default async function logout(): Promise<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRed
|
||||
// This helps us reuse one component for various methods (e.g., Google, Facebook, etc.).
|
||||
const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect)
|
||||
|
||||
exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect)
|
||||
exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect)
|
||||
return () => {
|
||||
firstRender.current = false
|
||||
}
|
||||
@ -47,22 +47,22 @@ function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRed
|
||||
return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}`
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
|
||||
const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect)
|
||||
async function exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
|
||||
const sessionId = await exchangeCodeForSessionId(apiServerUrlHandlingOauthRedirect)
|
||||
|
||||
if (token !== null) {
|
||||
await initSession(token)
|
||||
if (sessionId !== null) {
|
||||
await initSession(sessionId)
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
} else {
|
||||
console.error('Error obtaining JWT token')
|
||||
console.error('Error obtaining session ID')
|
||||
history.push('{= onAuthFailedRedirectTo =}')
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwt(url) {
|
||||
async function exchangeCodeForSessionId(url) {
|
||||
try {
|
||||
const response = await api.get(url)
|
||||
return response?.data?.token || null
|
||||
return response?.data?.sessionId || null
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
|
@ -1,2 +1,2 @@
|
||||
// todo(filip): turn into a proper import/path
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/'
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../../../server/src/_types/'
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { createContext, useState, useEffect } from 'react'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
import { getAuthToken } from '../api'
|
||||
import { getSessionId } from '../api'
|
||||
import { apiEventsEmitter } from '../api/events'
|
||||
import config from '../config'
|
||||
|
||||
@ -16,7 +16,7 @@ function refreshAuthToken() {
|
||||
// NOTE: When we figure out how `auth: true` works for Operations, we should
|
||||
// mirror that behavior here for WebSockets. Ref: https://github.com/wasp-lang/wasp/issues/1133
|
||||
socket.auth = {
|
||||
token: getAuthToken()
|
||||
sessionId: getSessionId()
|
||||
}
|
||||
|
||||
if (socket.connected) {
|
||||
@ -26,8 +26,8 @@ function refreshAuthToken() {
|
||||
}
|
||||
|
||||
refreshAuthToken()
|
||||
apiEventsEmitter.on('authToken.set', refreshAuthToken)
|
||||
apiEventsEmitter.on('authToken.clear', refreshAuthToken)
|
||||
apiEventsEmitter.on('sessionId.set', refreshAuthToken)
|
||||
apiEventsEmitter.on('sessionId.clear', refreshAuthToken)
|
||||
|
||||
export const WebSocketContext = createContext({
|
||||
socket,
|
||||
|
@ -88,18 +88,18 @@ type Context<Entities extends _Entity[]> = Expand<{
|
||||
{=# isAuthEnabled =}
|
||||
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { 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<Omit<{= authIdentityEntityName =}, 'providerData'> & {
|
||||
export type DeserializedAuthIdentity = Expand<Omit<{= authIdentityEntityName =}, 'providerData'> & {
|
||||
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
|
||||
}>
|
||||
|
||||
export type SanitizedUser = {= userEntityName =} & {
|
||||
{= authFieldOnUserEntityName =}: {= authEntityName =} & {
|
||||
{= identitiesFieldOnAuthEntityName =}: DeserializedAuthEntity[]
|
||||
{= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[]
|
||||
} | null
|
||||
}
|
||||
|
||||
|
12
waspc/data/Generator/templates/server/src/auth/jwt.ts
Normal file
12
waspc/data/Generator/templates/server/src/auth/jwt.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import util from 'util'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
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)
|
56
waspc/data/Generator/templates/server/src/auth/lucia.ts
Normal file
56
waspc/data/Generator/templates/server/src/auth/lucia.ts
Normal file
@ -0,0 +1,56 @@
|
||||
{{={= =}=}}
|
||||
import { Lucia } from "lucia";
|
||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import prisma from '../dbClient.js'
|
||||
import config from '../config.js'
|
||||
import { type {= userEntityUpper =} } 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.{= sessionEntityLower =} as any,
|
||||
prisma.{= authEntityLower =} 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: {= userEntityUpper =}['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: {= userEntityUpper =}['id']
|
||||
};
|
||||
}
|
||||
}
|
15
waspc/data/Generator/templates/server/src/auth/password.ts
Normal file
15
waspc/data/Generator/templates/server/src/auth/password.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import SecurePassword from 'secure-password'
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
|
||||
return hashedPwdBuffer.toString("base64")
|
||||
}
|
||||
|
||||
export const verifyPassword = async (hashedPassword: string, password: string): Promise<void> => {
|
||||
const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
if (result !== SecurePassword.VALID) {
|
||||
throw new Error('Invalid password.')
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { verifyPassword, throwInvalidCredentialsError } from "../../../core/auth.js";
|
||||
import { throwInvalidCredentialsError } from '../../utils.js'
|
||||
import { verifyPassword } from '../../password.js'
|
||||
import {
|
||||
createProviderId,
|
||||
findAuthIdentity,
|
||||
findAuthWithUserBy,
|
||||
createAuthToken,
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from "../../utils.js";
|
||||
import { ensureValidEmail, ensurePasswordIsPresent } from "../../validation.js";
|
||||
} from '../../utils.js'
|
||||
import { createSession } from '../../session.js'
|
||||
import { ensureValidEmail, ensurePasswordIsPresent } from '../../validation.js'
|
||||
|
||||
export function getLoginRoute({
|
||||
allowUnverifiedLogin,
|
||||
@ -17,7 +18,7 @@ export function getLoginRoute({
|
||||
return async function login(
|
||||
req: Request<{ email: string; password: string; }>,
|
||||
res: Response,
|
||||
): Promise<Response<{ token: string } | undefined>> {
|
||||
): Promise<Response<{ sessionId: string } | undefined>> {
|
||||
const fields = req.body ?? {}
|
||||
ensureValidArgs(fields)
|
||||
|
||||
@ -38,9 +39,11 @@ export function getLoginRoute({
|
||||
}
|
||||
|
||||
const auth = await findAuthWithUserBy({ id: authIdentity.authId })
|
||||
const token = await createAuthToken(auth.userId)
|
||||
const session = await createSession(auth.id)
|
||||
|
||||
return res.json({ token })
|
||||
return res.json({
|
||||
sessionId: session.id,
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,9 @@ export function getSignupRoute({
|
||||
await createUser(
|
||||
providerId,
|
||||
newUserProviderData,
|
||||
userFields,
|
||||
// Using any here because we want to avoid TypeScript errors and
|
||||
// rely on Prisma to validate the data.
|
||||
userFields as any
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
rethrowPossibleAuthError(e);
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{={= =}=}}
|
||||
import { signData } from '../../../core/auth.js'
|
||||
import { signData } from '../../jwt.js'
|
||||
import { emailSender } from '../../../email/index.js';
|
||||
import { Email } from '../../../email/core/types.js';
|
||||
import {
|
||||
|
@ -13,11 +13,11 @@ import {
|
||||
contextWithUserEntity,
|
||||
createUser,
|
||||
findAuthWithUserBy,
|
||||
createAuthToken,
|
||||
rethrowPossibleAuthError,
|
||||
sanitizeAndSerializeProviderData,
|
||||
} from "../../utils.js"
|
||||
import { type {= userEntityUpper =} } from "../../../entities/index.js"
|
||||
import { createSession } from "../../session.js"
|
||||
import { type {= authEntityUpper =} } from "../../../entities/index.js"
|
||||
import type { ProviderConfig, RequestWithWasp } from "../types.js"
|
||||
import type { GetUserFieldsFn } from "./types.js"
|
||||
import { handleRejection } from "../../../utils.js"
|
||||
@ -53,9 +53,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
|
||||
const providerId = createProviderId(provider.id, providerProfile.id);
|
||||
|
||||
try {
|
||||
const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
|
||||
const token = await createAuthToken(userId)
|
||||
res.json({ token })
|
||||
const authId = await getAuthIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
|
||||
const session = await createSession(authId)
|
||||
return res.json({
|
||||
sessionId: session.id,
|
||||
})
|
||||
} catch (e) {
|
||||
rethrowPossibleAuthError(e)
|
||||
}
|
||||
@ -67,11 +69,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
|
||||
|
||||
// We need a user id to create the auth token, so we either find an existing user
|
||||
// or create a new one if none exists for this provider.
|
||||
async function getUserIdFromProviderDetails(
|
||||
async function getAuthIdFromProviderDetails(
|
||||
providerId: ProviderId,
|
||||
providerProfile: any,
|
||||
getUserFieldsFn?: GetUserFieldsFn,
|
||||
): Promise<{= userEntityUpper =}['id']> {
|
||||
): Promise<{= authEntityUpper =}['id']> {
|
||||
const existingAuthIdentity = await prisma.{= authIdentityEntityLower =}.findUnique({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
@ -86,7 +88,7 @@ async function getUserIdFromProviderDetails(
|
||||
})
|
||||
|
||||
if (existingAuthIdentity) {
|
||||
return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.{= userFieldOnAuthEntityName =}.id
|
||||
return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.id
|
||||
} else {
|
||||
const userFields = getUserFieldsFn
|
||||
? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile })
|
||||
@ -101,6 +103,6 @@ async function getUserIdFromProviderDetails(
|
||||
userFields,
|
||||
)
|
||||
|
||||
return user.id
|
||||
return user.auth.id
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
{{={= =}=}}
|
||||
import { verifyPassword, throwInvalidCredentialsError } from '../../../core/auth.js'
|
||||
import { throwInvalidCredentialsError } from '../../utils.js'
|
||||
import { handleRejection } from '../../../utils.js'
|
||||
import { verifyPassword } from '../../password.js'
|
||||
|
||||
import {
|
||||
createProviderId,
|
||||
findAuthIdentity,
|
||||
findAuthWithUserBy,
|
||||
createAuthToken,
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from '../../utils.js'
|
||||
import { createSession } from '../../session.js'
|
||||
import { ensureValidUsername, ensurePasswordIsPresent } from '../../validation.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
@ -32,9 +33,12 @@ export default handleRejection(async (req, res) => {
|
||||
const auth = await findAuthWithUserBy({
|
||||
id: authIdentity.authId
|
||||
})
|
||||
const token = await createAuthToken(auth.userId)
|
||||
|
||||
return res.json({ token })
|
||||
const session = await createSession(auth.id)
|
||||
|
||||
return res.json({
|
||||
sessionId: session.id,
|
||||
})
|
||||
})
|
||||
|
||||
function ensureValidArgs(args: unknown): void {
|
||||
|
108
waspc/data/Generator/templates/server/src/auth/session.ts
Normal file
108
waspc/data/Generator/templates/server/src/auth/session.ts
Normal file
@ -0,0 +1,108 @@
|
||||
{{={= =}=}}
|
||||
import { Request as ExpressRequest } from "express";
|
||||
|
||||
import { type {= userEntityUpper =} } from "../entities/index.js"
|
||||
import { type SanitizedUser } from '../_types/index.js'
|
||||
|
||||
import { auth } from "./lucia.js";
|
||||
import type { Session } from "lucia";
|
||||
import {
|
||||
throwInvalidCredentialsError,
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from "./utils.js";
|
||||
|
||||
import prisma from '../dbClient.js';
|
||||
|
||||
// Creates a new session for the `authId` in the database
|
||||
export async function createSession(authId: string): Promise<Session> {
|
||||
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: {= userEntityUpper =}['id']): Promise<SanitizedUser> {
|
||||
const user = await prisma.{= userEntityLower =}
|
||||
.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
{= authFieldOnUserEntityName =}: {
|
||||
include: {
|
||||
{= identitiesFieldOnAuthEntityName =}: 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.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.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<void> {
|
||||
return auth.invalidateSession(sessionId);
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts
|
||||
// If you are changing the logic here, make sure to change it there as well.
|
||||
|
||||
import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index'
|
||||
import type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../_types/index'
|
||||
|
||||
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
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
{{={= =}=}}
|
||||
import { hashPassword, sign, verify } from '../core/auth.js'
|
||||
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 '../dbClient.js'
|
||||
@ -137,7 +138,9 @@ export async function createUser(
|
||||
providerId: ProviderId,
|
||||
serializedProviderData?: string,
|
||||
userFields?: PossibleAdditionalSignupFields,
|
||||
): Promise<{= userEntityUpper =}> {
|
||||
): Promise<{= userEntityUpper =} & {
|
||||
auth: {= authEntityUpper =}
|
||||
}> {
|
||||
return prisma.{= userEntityLower =}.create({
|
||||
data: {
|
||||
// Using any here to prevent type errors when userFields are not
|
||||
@ -154,7 +157,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: {
|
||||
{= authFieldOnUserEntityName =}: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -164,12 +172,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe
|
||||
} } })
|
||||
}
|
||||
|
||||
export async function createAuthToken(
|
||||
userId: {= userEntityUpper =}['id']
|
||||
): Promise<string> {
|
||||
return sign(userId);
|
||||
}
|
||||
|
||||
export async function verifyToken<T = unknown>(token: string): Promise<T> {
|
||||
return verify(token);
|
||||
}
|
||||
@ -297,3 +299,7 @@ function providerDataHasPasswordField(
|
||||
): providerData is { hashedPassword: string } {
|
||||
return 'hashedPassword' in providerData;
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message?: string): void {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
@ -1,24 +1,23 @@
|
||||
{{={= =}=}}
|
||||
import jwt from 'jsonwebtoken'
|
||||
import SecurePassword from 'secure-password'
|
||||
import util from 'util'
|
||||
import { randomInt } from 'node:crypto'
|
||||
|
||||
import prisma from '../dbClient.js'
|
||||
import { handleRejection } from '../utils.js'
|
||||
import HttpError from '../core/HttpError.js'
|
||||
import config from '../config.js'
|
||||
import { deserializeAndSanitizeProviderData } from '../auth/utils.js'
|
||||
|
||||
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 { getSessionAndUserFromBearerToken } from '../auth/session.js'
|
||||
import { throwInvalidCredentialsError } from '../auth/utils.js'
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@ -28,69 +27,18 @@ 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.{= userEntityLower =}
|
||||
.findUnique({
|
||||
where: { id: userIdFromToken },
|
||||
include: {
|
||||
{= authFieldOnUserEntityName =}: {
|
||||
include: {
|
||||
{= identitiesFieldOnAuthEntityName =}: 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.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =} = sanitizedUser.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.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() {
|
||||
@ -139,8 +87,4 @@ async function findAvailableUsername(potentialUsernames) {
|
||||
return availableUsernames[0]
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message) {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
||||
export default auth
|
||||
|
@ -18,7 +18,7 @@ import type {
|
||||
{= crud.entityUpper =},
|
||||
} from "../entities";
|
||||
{=# isAuthEnabled =}
|
||||
import { throwInvalidCredentialsError } from "../core/auth.js";
|
||||
import { throwInvalidCredentialsError } from '../auth/utils.js'
|
||||
{=/ isAuthEnabled =}
|
||||
{=# overrides.GetAll.isDefined =}
|
||||
{=& overrides.GetAll.importStatement =}
|
||||
|
7
waspc/data/Generator/templates/server/src/polyfill.ts
Normal file
7
waspc/data/Generator/templates/server/src/polyfill.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -3,12 +3,14 @@ import express from 'express'
|
||||
|
||||
import auth from '../../core/auth.js'
|
||||
import me from './me.js'
|
||||
import logout from './logout.js'
|
||||
|
||||
import providersRouter from '../../auth/providers/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/me', auth, me)
|
||||
router.post('/logout', auth, logout)
|
||||
router.use('/', providersRouter)
|
||||
|
||||
export default router
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { throwInvalidCredentialsError } from '../../auth/utils.js'
|
||||
import { invalidateSession } from '../../auth/session.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
if (req.sessionId) {
|
||||
await invalidateSession(req.sessionId)
|
||||
return res.json({ success: true })
|
||||
} else {
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
})
|
@ -1,7 +1,6 @@
|
||||
{{={= =}=}}
|
||||
import { serialize as superjsonSerialize } from 'superjson'
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { throwInvalidCredentialsError } from '../../core/auth.js'
|
||||
import { throwInvalidCredentialsError } from '../../auth/utils.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
if (req.user) {
|
||||
|
@ -18,6 +18,8 @@ import './jobs/core/allJobs.js'
|
||||
import { init as initWebSocket } from './webSocket/initialization.js'
|
||||
{=/ userWebSocketFn.isDefined =}
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
{=# isPgBossJobExecutorUsed =}
|
||||
await startPgBoss()
|
||||
|
@ -12,7 +12,8 @@ import { type SanitizedUser } from './_types/index.js'
|
||||
|
||||
type RequestWithExtraFields = Request & {
|
||||
{=# isAuthEnabled =}
|
||||
user?: SanitizedUser
|
||||
user?: SanitizedUser;
|
||||
sessionId?: string;
|
||||
{=/ isAuthEnabled =}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import config from '../config.js'
|
||||
import prisma from '../dbClient.js'
|
||||
|
||||
{=# isAuthEnabled =}
|
||||
import { getUserFromToken } from '../core/auth.js'
|
||||
import { getSessionAndUserFromSessionId } from '../auth/session.js'
|
||||
{=/ isAuthEnabled =}
|
||||
|
||||
{=& userWebSocketFn.importStatement =}
|
||||
@ -40,10 +40,11 @@ export async function init(server: http.Server): Promise<void> {
|
||||
|
||||
{=# isAuthEnabled =}
|
||||
async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) => void) {
|
||||
const token = socket.handshake.auth.token
|
||||
if (token) {
|
||||
const sessionId = socket.handshake.auth.sessionId
|
||||
if (sessionId) {
|
||||
try {
|
||||
socket.data = { ...socket.data, user: await getUserFromToken(token) }
|
||||
const { user } = await getSessionAndUserFromSessionId(sessionId)
|
||||
socket.data = { ...socket.data, user }
|
||||
} catch (err) { }
|
||||
}
|
||||
next()
|
||||
|
@ -23,6 +23,7 @@ waspBuild/.wasp/build/server/src/entities/index.ts
|
||||
waspBuild/.wasp/build/server/src/middleware/globalMiddleware.ts
|
||||
waspBuild/.wasp/build/server/src/middleware/index.ts
|
||||
waspBuild/.wasp/build/server/src/middleware/operations.ts
|
||||
waspBuild/.wasp/build/server/src/polyfill.ts
|
||||
waspBuild/.wasp/build/server/src/queries/types.ts
|
||||
waspBuild/.wasp/build/server/src/routes/index.js
|
||||
waspBuild/.wasp/build/server/src/routes/operations/index.js
|
||||
|
@ -167,6 +167,13 @@
|
||||
],
|
||||
"864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/polyfill.ts"
|
||||
],
|
||||
"66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -193,7 +200,7 @@
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be"
|
||||
"d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -326,14 +333,14 @@
|
||||
"file",
|
||||
"web-app/src/api.ts"
|
||||
],
|
||||
"93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf"
|
||||
"850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api/events.ts"
|
||||
],
|
||||
"7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044"
|
||||
"91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
7
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/polyfill.ts
generated
Normal file
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -6,6 +6,8 @@ import config from './config.js'
|
||||
|
||||
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
|
||||
const port = normalizePort(config.port)
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -24,6 +24,7 @@ waspCompile/.wasp/out/server/src/entities/index.ts
|
||||
waspCompile/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspCompile/.wasp/out/server/src/middleware/index.ts
|
||||
waspCompile/.wasp/out/server/src/middleware/operations.ts
|
||||
waspCompile/.wasp/out/server/src/polyfill.ts
|
||||
waspCompile/.wasp/out/server/src/queries/types.ts
|
||||
waspCompile/.wasp/out/server/src/routes/index.js
|
||||
waspCompile/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -174,6 +174,13 @@
|
||||
],
|
||||
"864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/polyfill.ts"
|
||||
],
|
||||
"66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -200,7 +207,7 @@
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be"
|
||||
"d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -340,14 +347,14 @@
|
||||
"file",
|
||||
"web-app/src/api.ts"
|
||||
],
|
||||
"93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf"
|
||||
"850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api/events.ts"
|
||||
],
|
||||
"7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044"
|
||||
"91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
7
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/polyfill.ts
generated
Normal file
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -6,6 +6,8 @@ import config from './config.js'
|
||||
|
||||
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
|
||||
const port = normalizePort(config.port)
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -21,12 +21,16 @@ waspComplexTest/.wasp/out/server/src/actions/MySpecialAction.ts
|
||||
waspComplexTest/.wasp/out/server/src/actions/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/apis/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/app.js
|
||||
waspComplexTest/.wasp/out/server/src/auth/jwt.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/lucia.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/password.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/config/google.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/createRouter.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/init.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/oauth/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/providers/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/session.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/user.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/utils.ts
|
||||
waspComplexTest/.wasp/out/server/src/auth/validation.ts
|
||||
@ -60,10 +64,12 @@ waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.ts
|
||||
waspComplexTest/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspComplexTest/.wasp/out/server/src/middleware/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/middleware/operations.ts
|
||||
waspComplexTest/.wasp/out/server/src/polyfill.ts
|
||||
waspComplexTest/.wasp/out/server/src/queries/MySpecialQuery.ts
|
||||
waspComplexTest/.wasp/out/server/src/queries/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/apis/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/index.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/logout.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/auth/me.js
|
||||
waspComplexTest/.wasp/out/server/src/routes/crud/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/crud/tasks.ts
|
||||
|
@ -18,7 +18,7 @@
|
||||
"file",
|
||||
"db/schema.prisma"
|
||||
],
|
||||
"3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393"
|
||||
"5a41561cd6f853da2f4b9f64267f5d4e3b2865f94dfa92d9143acf2c1485519c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -60,7 +60,7 @@
|
||||
"file",
|
||||
"server/package.json"
|
||||
],
|
||||
"53aef3360681af1936a5bb2a3f7fa1c7cb74f58f398942bb8bd979a9b78e9b16"
|
||||
"831f2505c29201cd8e5076fda768423448a6cbba92410b0692fd8ac630aeb76b"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -88,7 +88,7 @@
|
||||
"file",
|
||||
"server/src/_types/index.ts"
|
||||
],
|
||||
"92027caebe484c7d412f97be9bc3c39257d10f9f2bc2e447e52c6855d5d30ffc"
|
||||
"e7678be590a77799133e99d07a3326bbc6e627a5a219db8714a1b3d3b3304150"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -132,6 +132,27 @@
|
||||
],
|
||||
"86504ede1daeca35cc93f665ca8ac2fdf46ecaff02f6d3a7810a14d2bc71e16a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/jwt.ts"
|
||||
],
|
||||
"be637e3c0ac601c1b6edb4a5cc6a9300edfb7fb1568220672254524a6b395b89"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/lucia.ts"
|
||||
],
|
||||
"dc5e75c3e3da677c8e98d1456964f1f9bce9105bf90f3c351379e54d3433523b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/password.ts"
|
||||
],
|
||||
"f9003f11bf1396bff69a98440543e6e131f50d5bc1269a8aae55d72615922705"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -151,7 +172,7 @@
|
||||
"file",
|
||||
"server/src/auth/providers/oauth/createRouter.ts"
|
||||
],
|
||||
"da122a8a244ddbd9b84bba72a7da2f32ffa41c6093614c6c0d59e113244d2bbc"
|
||||
"90fe889802b86a96e82acfd9ace8b79d144bb33369236ac315b4dd67e31a9f69"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -174,19 +195,26 @@
|
||||
],
|
||||
"b647575a04eeb7824d95082a461d59763d034dc7d03a8fbcdd25143b6f8431b6"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/session.ts"
|
||||
],
|
||||
"6edca533dbb38ddcba326fd0399dc6f5bc9b249327ed61c7c167d3a5a9d9a462"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/user.ts"
|
||||
],
|
||||
"5787f3cdab4739781090f2950ba432dca812483ec23c6319ac3f876118324d15"
|
||||
"74080241b9011ddce54ac2452b7c8a7064a4070ef1b04e420c730c62d787e077"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/auth/utils.ts"
|
||||
],
|
||||
"ba76300456ffdbd647923b27ff163df5e3efa016d7cd2a01af0b6a86dcd780a9"
|
||||
"0b9bb32aac75246f649b01dca5545b2af3c4485a3196a17b91e2556be4891f9a"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -221,14 +249,14 @@
|
||||
"file",
|
||||
"server/src/core/auth.js"
|
||||
],
|
||||
"d708303af170e8159b93f0dda521b6f622c0f3add2d4f4f8f2fd88c0a4f7b79e"
|
||||
"9eec6454bb4f583dec14b7fb7a3f2d802f536bad8cb15390b9800fe6dbccc596"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/crud/tasks.ts"
|
||||
],
|
||||
"2c4e1f94939adf825df14624940019889394a0e56cdea2855686d67e0c08458a"
|
||||
"3dbc7ee4341bc00af125c02d4b771bc7e7fb5bc7063191d659e7243217e2761f"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -405,6 +433,13 @@
|
||||
],
|
||||
"64eeed927f46f6d6eba143023f25fb9ac4cd81d6b68c9a7067306ad28a3eda92"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/polyfill.ts"
|
||||
],
|
||||
"66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -431,14 +466,21 @@
|
||||
"file",
|
||||
"server/src/routes/auth/index.js"
|
||||
],
|
||||
"47fb3317c5707e0d7646bb1c0f6fa8d9c5c0f980ca2d643d226e63b49766cea3"
|
||||
"9761e5af295928520748246d3be0ba4113655dbef3afb5c9123894627f3bee1a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/logout.ts"
|
||||
],
|
||||
"1324b110888548ab535d78a4c7614c996152e948144a873d0262fa0aeb7ab4dc"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/routes/auth/me.js"
|
||||
],
|
||||
"705f77d8970a8367981c9a89601c6d5b12e998c23970ae1735b376dd0826ef10"
|
||||
"9a9cb533bb94af63caf448f73a0d0fef8902c8f8d1af411bed2570a32da2fab9"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -487,7 +529,7 @@
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"93c05fac0fb2e30eeda90dbb374bfa5c7fcb860b4605da8ae2c6b6f913f95963"
|
||||
"7963a3e625deb86593258b01dd87c94cf4e2103dbda3a6ee82e672911dee09bc"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -515,7 +557,7 @@
|
||||
"file",
|
||||
"server/src/utils.ts"
|
||||
],
|
||||
"f8834df362946064f32ef6a145769f83d10da712ad3daa226243fc590f89618f"
|
||||
"7d29cb34de86e6a0689655e4165aed9fe5b0f82f54e4194f002f7d5823c7cb18"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -634,14 +676,14 @@
|
||||
"file",
|
||||
"web-app/src/api.ts"
|
||||
],
|
||||
"93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf"
|
||||
"850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api/events.ts"
|
||||
],
|
||||
"7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044"
|
||||
"91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -718,21 +760,21 @@
|
||||
"file",
|
||||
"web-app/src/auth/helpers/user.ts"
|
||||
],
|
||||
"e6bc091d8f8520db542f959846ecf528e8a070c5ce989151d00d2f45da4a58a6"
|
||||
"e57cecd0a50b1515d6da8fcfb45f1e98ebefa4f58fe59d792100ffd40980fee9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/logout.ts"
|
||||
],
|
||||
"6717411aa38e54aa74a5034628510ee3ca0f2ea9d1692644ee4886c74d8657af"
|
||||
"e04607e4676af958f580152432e873452a33a4276ecdb1a4c368efe2e3958a6c"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/pages/OAuthCodeExchange.jsx"
|
||||
],
|
||||
"7dbcc288201aafbb50b5f5319a28283546c81d006fe61c2a8a3c5f55c6833fb2"
|
||||
"1536a0abfd92944dca315e344758ba50f44a18e34c18a8c1c91046b1e4cc2a5e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -746,7 +788,7 @@
|
||||
"file",
|
||||
"web-app/src/auth/types.ts"
|
||||
],
|
||||
"5ce8d0493c362093b0b2fc7b9df78a86688d3f40264ea8f29530f1d8fa67c4c6"
|
||||
"26b1d53d6ea48d56421ee7050486eb9086367195534f21ca3371cd0685307d7c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -760,7 +802,7 @@
|
||||
"file",
|
||||
"web-app/src/auth/user.ts"
|
||||
],
|
||||
"7113c286081f5597b822f5e576735d321cce38fcbd1a25db0d90e1163570068f"
|
||||
"08cd2cf7dbb5aa5371efcfc1b16651c0c59d03d4ea36cd9f69bdaa9edc5ca68e"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -38,6 +38,7 @@ model Auth {
|
||||
userId Int? @unique
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
identities AuthIdentity[]
|
||||
sessions Session[]
|
||||
|
||||
}
|
||||
model AuthIdentity {
|
||||
@ -49,3 +50,11 @@ model AuthIdentity {
|
||||
@@id([providerName, providerUserId])
|
||||
|
||||
}
|
||||
model Session {
|
||||
id String @id @unique
|
||||
expiresAt DateTime
|
||||
userId String
|
||||
auth Auth @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
@@index([userId])
|
||||
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
3e6bfc3dadfc9399a3fae3ea77cadb1aa3259efa0b8362ed77d6d5ee5013d393
|
||||
5a41561cd6f853da2f4b9f64267f5d4e3b2865f94dfa92d9143acf2c1485519c
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.16.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.16.2"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.0.0"},{"name":"@tsconfig/node18","version":"latest"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.16.2"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.16.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"rate-limiter-flexible","version":"^2.4.1"},{"name":"superjson","version":"^1.12.2"},{"name":"lucia","version":"^3.0.0-beta.14"},{"name":"@lucia-auth/adapter-prisma","version":"^4.0.0-beta.9"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.16.2"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.0.0"},{"name":"@tsconfig/node18","version":"latest"},{"name":"@types/uuid","version":"^9.0.0"},{"name":"@types/cors","version":"^2.8.5"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^1.4.0"},{"name":"react","version":"^18.2.0"},{"name":"react-dom","version":"^18.2.0"},{"name":"@tanstack/react-query","version":"^4.29.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.16.2"},{"name":"superjson","version":"^1.12.2"},{"name":"mitt","version":"3.0.0"},{"name":"react-hook-form","version":"^7.45.4"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.3.9"},{"name":"typescript","version":"^5.1.0"},{"name":"@types/react","version":"^18.0.37"},{"name":"@types/react-dom","version":"^18.0.11"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^2.0.0"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^14.0.0"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@lucia-auth/adapter-prisma": "^4.0.0-beta.9",
|
||||
"@prisma/client": "4.16.2",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"cookie-parser": "~1.4.6",
|
||||
@ -9,6 +10,7 @@
|
||||
"helmet": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lucia": "^3.0.0-beta.14",
|
||||
"morgan": "~1.10.0",
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
|
@ -82,18 +82,18 @@ type Context<Entities extends _Entity[]> = Expand<{
|
||||
|
||||
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { 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<Omit<AuthIdentity, 'providerData'> & {
|
||||
export type DeserializedAuthIdentity = Expand<Omit<AuthIdentity, 'providerData'> & {
|
||||
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
|
||||
}>
|
||||
|
||||
export type SanitizedUser = User & {
|
||||
auth: Auth & {
|
||||
identities: DeserializedAuthEntity[]
|
||||
identities: DeserializedAuthIdentity[]
|
||||
} | null
|
||||
}
|
||||
|
||||
|
12
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts
generated
Normal file
12
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/jwt.ts
generated
Normal file
@ -0,0 +1,12 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import util from 'util'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
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)
|
@ -0,0 +1,55 @@
|
||||
import { Lucia } from "lucia";
|
||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import prisma from '../dbClient.js'
|
||||
import config from '../config.js'
|
||||
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']
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import SecurePassword from 'secure-password'
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
|
||||
return hashedPwdBuffer.toString("base64")
|
||||
}
|
||||
|
||||
export const verifyPassword = async (hashedPassword: string, password: string): Promise<void> => {
|
||||
const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
if (result !== SecurePassword.VALID) {
|
||||
throw new Error('Invalid password.')
|
||||
}
|
||||
}
|
@ -12,11 +12,11 @@ import {
|
||||
contextWithUserEntity,
|
||||
createUser,
|
||||
findAuthWithUserBy,
|
||||
createAuthToken,
|
||||
rethrowPossibleAuthError,
|
||||
sanitizeAndSerializeProviderData,
|
||||
} from "../../utils.js"
|
||||
import { type User } from "../../../entities/index.js"
|
||||
import { createSession } from "../../session.js"
|
||||
import { type Auth } from "../../../entities/index.js"
|
||||
import type { ProviderConfig, RequestWithWasp } from "../types.js"
|
||||
import type { GetUserFieldsFn } from "./types.js"
|
||||
import { handleRejection } from "../../../utils.js"
|
||||
@ -52,9 +52,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
|
||||
const providerId = createProviderId(provider.id, providerProfile.id);
|
||||
|
||||
try {
|
||||
const userId = await getUserIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
|
||||
const token = await createAuthToken(userId)
|
||||
res.json({ token })
|
||||
const authId = await getAuthIdFromProviderDetails(providerId, providerProfile, getUserFieldsFn)
|
||||
const session = await createSession(authId)
|
||||
return res.json({
|
||||
sessionId: session.id,
|
||||
})
|
||||
} catch (e) {
|
||||
rethrowPossibleAuthError(e)
|
||||
}
|
||||
@ -66,11 +68,11 @@ export function createRouter(provider: ProviderConfig, initData: { passportStrat
|
||||
|
||||
// We need a user id to create the auth token, so we either find an existing user
|
||||
// or create a new one if none exists for this provider.
|
||||
async function getUserIdFromProviderDetails(
|
||||
async function getAuthIdFromProviderDetails(
|
||||
providerId: ProviderId,
|
||||
providerProfile: any,
|
||||
getUserFieldsFn?: GetUserFieldsFn,
|
||||
): Promise<User['id']> {
|
||||
): Promise<Auth['id']> {
|
||||
const existingAuthIdentity = await prisma.authIdentity.findUnique({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
@ -85,7 +87,7 @@ async function getUserIdFromProviderDetails(
|
||||
})
|
||||
|
||||
if (existingAuthIdentity) {
|
||||
return existingAuthIdentity.auth.user.id
|
||||
return existingAuthIdentity.auth.id
|
||||
} else {
|
||||
const userFields = getUserFieldsFn
|
||||
? await getUserFieldsFn(contextWithUserEntity, { profile: providerProfile })
|
||||
@ -100,6 +102,6 @@ async function getUserIdFromProviderDetails(
|
||||
userFields,
|
||||
)
|
||||
|
||||
return user.id
|
||||
return user.auth.id
|
||||
}
|
||||
}
|
||||
|
107
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts
generated
Normal file
107
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/.wasp/out/server/src/auth/session.ts
generated
Normal file
@ -0,0 +1,107 @@
|
||||
import { Request as ExpressRequest } from "express";
|
||||
|
||||
import { type User } from "../entities/index.js"
|
||||
import { type SanitizedUser } from '../_types/index.js'
|
||||
|
||||
import { auth } from "./lucia.js";
|
||||
import type { Session } from "lucia";
|
||||
import {
|
||||
throwInvalidCredentialsError,
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from "./utils.js";
|
||||
|
||||
import prisma from '../dbClient.js';
|
||||
|
||||
// Creates a new session for the `authId` in the database
|
||||
export async function createSession(authId: string): Promise<Session> {
|
||||
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<SanitizedUser> {
|
||||
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<void> {
|
||||
return auth.invalidateSession(sessionId);
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// We have them duplicated in this file and in data/Generator/templates/react-app/src/auth/user.ts
|
||||
// If you are changing the logic here, make sure to change it there as well.
|
||||
|
||||
import type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../_types/index'
|
||||
import type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../_types/index'
|
||||
|
||||
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
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { hashPassword, sign, verify } from '../core/auth.js'
|
||||
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 '../dbClient.js'
|
||||
@ -128,7 +129,9 @@ export async function createUser(
|
||||
providerId: ProviderId,
|
||||
serializedProviderData?: string,
|
||||
userFields?: PossibleAdditionalSignupFields,
|
||||
): Promise<User> {
|
||||
): Promise<User & {
|
||||
auth: Auth
|
||||
}> {
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
// Using any here to prevent type errors when userFields are not
|
||||
@ -145,7 +148,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 +163,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe
|
||||
} } })
|
||||
}
|
||||
|
||||
export async function createAuthToken(
|
||||
userId: User['id']
|
||||
): Promise<string> {
|
||||
return sign(userId);
|
||||
}
|
||||
|
||||
export async function verifyToken<T = unknown>(token: string): Promise<T> {
|
||||
return verify(token);
|
||||
}
|
||||
@ -288,3 +290,7 @@ function providerDataHasPasswordField(
|
||||
): providerData is { hashedPassword: string } {
|
||||
return 'hashedPassword' in providerData;
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message?: string): void {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
@ -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 '../dbClient.js'
|
||||
import { handleRejection } from '../utils.js'
|
||||
import HttpError from '../core/HttpError.js'
|
||||
import config from '../config.js'
|
||||
import { deserializeAndSanitizeProviderData } from '../auth/utils.js'
|
||||
|
||||
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 { getSessionAndUserFromBearerToken } from '../auth/session.js'
|
||||
import { throwInvalidCredentialsError } from '../auth/utils.js'
|
||||
|
||||
/**
|
||||
* 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,69 +26,18 @@ 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() {
|
||||
@ -138,8 +86,4 @@ async function findAvailableUsername(potentialUsernames) {
|
||||
return availableUsernames[0]
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message) {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
||||
export default auth
|
||||
|
@ -10,7 +10,7 @@ import { Payload } from "../_types/serialization.js";
|
||||
import type {
|
||||
Task,
|
||||
} from "../entities";
|
||||
import { throwInvalidCredentialsError } from "../core/auth.js";
|
||||
import { throwInvalidCredentialsError } from '../auth/utils.js'
|
||||
|
||||
type _WaspEntityTagged = _Task
|
||||
type _WaspEntity = Task
|
||||
|
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -2,12 +2,14 @@ import express from 'express'
|
||||
|
||||
import auth from '../../core/auth.js'
|
||||
import me from './me.js'
|
||||
import logout from './logout.js'
|
||||
|
||||
import providersRouter from '../../auth/providers/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/me', auth, me)
|
||||
router.post('/logout', auth, logout)
|
||||
router.use('/', providersRouter)
|
||||
|
||||
export default router
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { throwInvalidCredentialsError } from '../../auth/utils.js'
|
||||
import { invalidateSession } from '../../auth/session.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
if (req.sessionId) {
|
||||
await invalidateSession(req.sessionId)
|
||||
return res.json({ success: true })
|
||||
} else {
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import { serialize as superjsonSerialize } from 'superjson'
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { throwInvalidCredentialsError } from '../../core/auth.js'
|
||||
import { throwInvalidCredentialsError } from '../../auth/utils.js'
|
||||
|
||||
export default handleRejection(async (req, res) => {
|
||||
if (req.user) {
|
||||
|
@ -10,6 +10,8 @@ import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js'
|
||||
import './jobs/core/allJobs.js'
|
||||
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
await startPgBoss()
|
||||
|
||||
|
@ -8,7 +8,8 @@ import { fileURLToPath } from 'url'
|
||||
import { type SanitizedUser } from './_types/index.js'
|
||||
|
||||
type RequestWithExtraFields = Request & {
|
||||
user?: SanitizedUser
|
||||
user?: SanitizedUser;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { setAuthToken } from '../../api'
|
||||
import { setSessionId } from '../../api'
|
||||
import { invalidateAndRemoveQueries } from '../../operations/resources'
|
||||
|
||||
export async function initSession(token: string): Promise<void> {
|
||||
setAuthToken(token)
|
||||
export async function initSession(sessionId: string): Promise<void> {
|
||||
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.
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { removeLocalUserData } from '../api'
|
||||
import api, { removeLocalUserData } from '../api'
|
||||
import { invalidateAndRemoveQueries } from '../operations/resources'
|
||||
|
||||
export default async function logout(): Promise<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRed
|
||||
// This helps us reuse one component for various methods (e.g., Google, Facebook, etc.).
|
||||
const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect)
|
||||
|
||||
exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect)
|
||||
exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect)
|
||||
return () => {
|
||||
firstRender.current = false
|
||||
}
|
||||
@ -46,22 +46,22 @@ function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRed
|
||||
return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}`
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
|
||||
const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect)
|
||||
async function exchangeCodeForSessionIdAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
|
||||
const sessionId = await exchangeCodeForSessionId(apiServerUrlHandlingOauthRedirect)
|
||||
|
||||
if (token !== null) {
|
||||
await initSession(token)
|
||||
if (sessionId !== null) {
|
||||
await initSession(sessionId)
|
||||
history.push('/')
|
||||
} else {
|
||||
console.error('Error obtaining JWT token')
|
||||
console.error('Error obtaining session ID')
|
||||
history.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeCodeForJwt(url) {
|
||||
async function exchangeCodeForSessionId(url) {
|
||||
try {
|
||||
const response = await api.get(url)
|
||||
return response?.data?.token || null
|
||||
return response?.data?.sessionId || null
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
|
@ -1,2 +1,2 @@
|
||||
// todo(filip): turn into a proper import/path
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from '../../../server/src/_types/'
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../../../server/src/_types/'
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -31,6 +31,7 @@ waspJob/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.ts
|
||||
waspJob/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspJob/.wasp/out/server/src/middleware/index.ts
|
||||
waspJob/.wasp/out/server/src/middleware/operations.ts
|
||||
waspJob/.wasp/out/server/src/polyfill.ts
|
||||
waspJob/.wasp/out/server/src/queries/types.ts
|
||||
waspJob/.wasp/out/server/src/routes/index.js
|
||||
waspJob/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -216,6 +216,13 @@
|
||||
],
|
||||
"864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/polyfill.ts"
|
||||
],
|
||||
"66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -242,7 +249,7 @@
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"e28a2e72f8a0cedbfba8c6acfbc36b9ae35db9005aa26484d88ef7e7688efa5b"
|
||||
"d26cf3913b82c3525fe2214e0d94606e36ccb306ca9b036979e003f1e1d44f4b"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -382,14 +389,14 @@
|
||||
"file",
|
||||
"web-app/src/api.ts"
|
||||
],
|
||||
"93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf"
|
||||
"850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api/events.ts"
|
||||
],
|
||||
"7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044"
|
||||
"91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
7
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/polyfill.ts
generated
Normal file
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -8,6 +8,8 @@ import { startPgBoss } from './jobs/core/pgBoss/pgBoss.js'
|
||||
import './jobs/core/allJobs.js'
|
||||
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
await startPgBoss()
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -29,6 +29,7 @@ waspMigrate/.wasp/out/server/src/entities/index.ts
|
||||
waspMigrate/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspMigrate/.wasp/out/server/src/middleware/index.ts
|
||||
waspMigrate/.wasp/out/server/src/middleware/operations.ts
|
||||
waspMigrate/.wasp/out/server/src/polyfill.ts
|
||||
waspMigrate/.wasp/out/server/src/queries/types.ts
|
||||
waspMigrate/.wasp/out/server/src/routes/index.js
|
||||
waspMigrate/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -174,6 +174,13 @@
|
||||
],
|
||||
"864c7492c27f6da1e67645fbc358dc803a168852bfd24f2c4dd13fccf6917b07"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/polyfill.ts"
|
||||
],
|
||||
"66d3dca514bdd01be402714d0dfe3836e76f612346dea57ee595ae4f3da915cf"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -200,7 +207,7 @@
|
||||
"file",
|
||||
"server/src/server.ts"
|
||||
],
|
||||
"1c5af223cf0309b341e87cf8b6afd58b0cb21217e64cd9ee498048136a9da5be"
|
||||
"d0666b659cdc75db181ea2bbb50c4e157f0a7fbe00c4ff8fda0933b1a13e5a0e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -340,14 +347,14 @@
|
||||
"file",
|
||||
"web-app/src/api.ts"
|
||||
],
|
||||
"93118387834981574ce1773d33275308e68ef8ca87408a35be8931c44a8889bf"
|
||||
"850331885230117aa56317186c6d38f696fb1fbd0c56470ff7c6e4f3c1c43104"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/api/events.ts"
|
||||
],
|
||||
"7220e570cfb823028ad6c076cbcf033d217acfb88537bcac47020f1085757044"
|
||||
"91ec1889f649b608ca81cab8f048538b9dcc70f49444430b1e5b572af2a4970a"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
7
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts
generated
Normal file
7
waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/polyfill.ts
generated
Normal file
@ -0,0 +1,7 @@
|
||||
// This is a polyfill for Node.js 18 webcrypto API so Lucia can use it
|
||||
// for random number generation.
|
||||
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.crypto = webcrypto as Crypto;
|
@ -6,6 +6,8 @@ import config from './config.js'
|
||||
|
||||
|
||||
|
||||
import './polyfill.js'
|
||||
|
||||
const startServer = async () => {
|
||||
|
||||
const port = normalizePort(config.port)
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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<ApiEvents> = mitt<ApiEvents>();
|
||||
|
@ -1,24 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"address" TEXT,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Task" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,38 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Auth" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"username" TEXT,
|
||||
"password" TEXT,
|
||||
"isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"emailVerificationSentAt" TIMESTAMP(3),
|
||||
"passwordResetSentAt" TIMESTAMP(3),
|
||||
"userId" INTEGER,
|
||||
|
||||
CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SocialAuthProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"authId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "SocialAuthProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Auth_email_key" ON "Auth"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Auth_username_key" ON "Auth"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SocialAuthProvider" ADD CONSTRAINT "SocialAuthProvider_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `SocialAuthProvider` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "SocialAuthProvider" DROP CONSTRAINT "SocialAuthProvider_authId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "SocialAuthProvider";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuthIdentity" (
|
||||
"providerName" TEXT NOT NULL,
|
||||
"providerUserId" TEXT NOT NULL,
|
||||
"providerData" TEXT NOT NULL DEFAULT '{}',
|
||||
"authId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `email` on the `Auth` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `emailVerificationSentAt` on the `Auth` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `isEmailVerified` on the `Auth` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `password` on the `Auth` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `passwordResetSentAt` on the `Auth` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `username` on the `Auth` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "Auth_email_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Auth_username_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Auth" DROP COLUMN "email",
|
||||
DROP COLUMN "emailVerificationSentAt",
|
||||
DROP COLUMN "isEmailVerified",
|
||||
DROP COLUMN "password",
|
||||
DROP COLUMN "passwordResetSentAt",
|
||||
DROP COLUMN "username";
|
@ -1,13 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "User_username_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "password",
|
||||
DROP COLUMN "username";
|
@ -0,0 +1,64 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"address" TEXT,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Task" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Auth" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" INTEGER,
|
||||
|
||||
CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuthIdentity" (
|
||||
"providerName" TEXT NOT NULL,
|
||||
"providerUserId" TEXT NOT NULL,
|
||||
"providerData" TEXT NOT NULL DEFAULT '{}',
|
||||
"authId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 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");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -11,7 +11,6 @@ import { sanitizeAndSerializeProviderData } from '@wasp/auth/utils.js'
|
||||
|
||||
export const fields = defineAdditionalSignupFields({
|
||||
address: (data) => {
|
||||
console.log('Received data:', data)
|
||||
const AddressSchema = z
|
||||
.string({
|
||||
required_error: 'Address is required',
|
||||
|
@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -1,7 +1,7 @@
|
||||
import { createTask } from './actions.js'
|
||||
import type { DbSeedFn } from '@wasp/dbSeed/types.js'
|
||||
import { PrismaClient } from '@prisma/client/index.js'
|
||||
import { hashPassword } from '@wasp/core/auth.js'
|
||||
import { hashPassword } from '@wasp/auth/password.js'
|
||||
|
||||
async function createUser(prismaClient: PrismaClient, data: any) {
|
||||
const newUser = await prismaClient.user.create({
|
||||
|
@ -12,6 +12,7 @@ import Wasp.Generator.Monad
|
||||
)
|
||||
import qualified Wasp.Psl.Ast.Model as Psl.Model
|
||||
import qualified Wasp.Psl.Ast.Model as Psl.Model.Field
|
||||
import qualified Wasp.Util as Util
|
||||
|
||||
{--
|
||||
|
||||
@ -59,12 +60,22 @@ authIdentityEntityName = "AuthIdentity"
|
||||
identitiesFieldOnAuthEntityName :: String
|
||||
identitiesFieldOnAuthEntityName = "identities"
|
||||
|
||||
sessionEntityName :: String
|
||||
sessionEntityName = "Session"
|
||||
|
||||
sessionsFieldOnAuthEntityName :: String
|
||||
sessionsFieldOnAuthEntityName = "sessions"
|
||||
|
||||
authFieldOnSessionEntityName :: String
|
||||
authFieldOnSessionEntityName = Util.toLowerFirst authEntityName
|
||||
|
||||
injectAuth :: [(String, AS.Entity.Entity)] -> (String, AS.Entity.Entity) -> Generator [(String, AS.Entity.Entity)]
|
||||
injectAuth entities (userEntityName, userEntity) = do
|
||||
authEntity <- makeAuthEntity userEntityIdField (userEntityName, userEntity)
|
||||
authIdentityEntity <- makeAuthIdentityEntity
|
||||
sessionEntity <- makeSessionEntity
|
||||
let entitiesWithAuth = injectAuthIntoUserEntity userEntityName entities
|
||||
return $ entitiesWithAuth ++ [authEntity, authIdentityEntity]
|
||||
return $ entitiesWithAuth ++ [authEntity, authIdentityEntity, sessionEntity]
|
||||
where
|
||||
-- We validated the AppSpec so we are sure that the user entity has an id field.
|
||||
userEntityIdField = fromJust $ AS.Entity.getIdField userEntity
|
||||
@ -94,7 +105,7 @@ makeAuthIdentityEntity = case parsePslBody authIdentityPslBody of
|
||||
|
||||
makeAuthEntity :: Psl.Model.Field -> (String, AS.Entity.Entity) -> Generator (String, AS.Entity.Entity)
|
||||
makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEntityPslBody of
|
||||
Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating Auth entity: " ++ show err
|
||||
Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating " ++ authEntityName ++ " entity: " ++ show err
|
||||
Right pslBody -> return (authEntityName, AS.Entity.makeEntity pslBody)
|
||||
where
|
||||
authEntityPslBody =
|
||||
@ -104,6 +115,7 @@ makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEnt
|
||||
userId ${userEntityIdTypeText}? @unique
|
||||
${userFieldOnAuthEntityNameText} ${userEntityNameText}? @relation(fields: [userId], references: [${userEntityIdFieldName}], onDelete: Cascade)
|
||||
${identitiesFieldOnAuthEntityNameText} ${authIdentityEntityNameText}[]
|
||||
${sessionsFieldOnAuthEntityNameText} ${sessionEntityNameText}[]
|
||||
|]
|
||||
|
||||
authEntityIdTypeText = T.pack authEntityIdType
|
||||
@ -111,10 +123,35 @@ makeAuthEntity userEntityIdField (userEntityName, _) = case parsePslBody authEnt
|
||||
userFieldOnAuthEntityNameText = T.pack userFieldOnAuthEntityName
|
||||
authIdentityEntityNameText = T.pack authIdentityEntityName
|
||||
identitiesFieldOnAuthEntityNameText = T.pack identitiesFieldOnAuthEntityName
|
||||
sessionsFieldOnAuthEntityNameText = T.pack sessionsFieldOnAuthEntityName
|
||||
sessionEntityNameText = T.pack sessionEntityName
|
||||
|
||||
userEntityIdTypeText = T.pack $ show . Psl.Model.Field._type $ userEntityIdField
|
||||
userEntityIdFieldName = T.pack $ Psl.Model.Field._name userEntityIdField
|
||||
|
||||
makeSessionEntity :: Generator (String, AS.Entity.Entity)
|
||||
makeSessionEntity = case parsePslBody sessionEntityPslBody of
|
||||
Left err -> logAndThrowGeneratorError $ GenericGeneratorError $ "Error while generating " ++ sessionEntityName ++ " entity: " ++ show err
|
||||
Right pslBody -> return (sessionEntityName, AS.Entity.makeEntity pslBody)
|
||||
where
|
||||
sessionEntityPslBody =
|
||||
T.unpack
|
||||
[trimming|
|
||||
id String @id @unique
|
||||
expiresAt DateTime
|
||||
|
||||
// Needs to be called `userId` for Lucia to be able to create sessions
|
||||
userId String
|
||||
// The relation needs to be named as lowercased entity name, because that's what Lucia expects.
|
||||
// If the entity is named `Foo`, the relation needs to be named `foo`.
|
||||
${authFieldOnSessionEntityNameText} ${authEntityNameText} @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
|]
|
||||
|
||||
authEntityNameText = T.pack authEntityName
|
||||
authFieldOnSessionEntityNameText = T.pack authFieldOnSessionEntityName
|
||||
|
||||
injectAuthIntoUserEntity :: String -> [(String, AS.Entity.Entity)] -> [(String, AS.Entity.Entity)]
|
||||
injectAuthIntoUserEntity userEntityName entities =
|
||||
let userEntity = fromJust $ lookup userEntityName entities
|
||||
|
@ -54,7 +54,7 @@ import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.NpmDependencies as N
|
||||
import Wasp.Generator.ServerGenerator.ApiRoutesG (genApis)
|
||||
import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (depsRequiredByPassport)
|
||||
import Wasp.Generator.ServerGenerator.AuthG (genAuth)
|
||||
import Wasp.Generator.ServerGenerator.AuthG (depsRequiredByAuth, genAuth)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
|
||||
import Wasp.Generator.ServerGenerator.CrudG (genCrud)
|
||||
@ -178,6 +178,7 @@ npmDepsForWasp spec =
|
||||
("rate-limiter-flexible", "^2.4.1"),
|
||||
("superjson", "^1.12.2")
|
||||
]
|
||||
++ depsRequiredByAuth spec
|
||||
++ depsRequiredByPassport spec
|
||||
++ depsRequiredByJobs spec
|
||||
++ depsRequiredByEmail spec
|
||||
@ -225,7 +226,8 @@ genSrcDir spec =
|
||||
genFileCopy [relfile|core/HttpError.js|],
|
||||
genDbClient spec,
|
||||
genConfigFile spec,
|
||||
genServerJs spec
|
||||
genServerJs spec,
|
||||
genFileCopy [relfile|polyfill.ts|]
|
||||
]
|
||||
<++> genServerUtils spec
|
||||
<++> genRoutesDir spec
|
||||
|
@ -1,5 +1,6 @@
|
||||
module Wasp.Generator.ServerGenerator.AuthG
|
||||
( genAuth,
|
||||
depsRequiredByAuth,
|
||||
)
|
||||
where
|
||||
|
||||
@ -19,6 +20,7 @@ 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 qualified Wasp.AppSpec.App.Dependency as AS.Dependency
|
||||
import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.AuthProviders (emailAuthProvider, gitHubAuthProvider, googleAuthProvider, localAuthProvider)
|
||||
import qualified Wasp.Generator.AuthProviders.Email as EmailProvider
|
||||
@ -41,12 +43,17 @@ genAuth spec = case maybeAuth of
|
||||
sequence
|
||||
[ genCoreAuth auth,
|
||||
genAuthRoutesIndex auth,
|
||||
genMeRoute auth,
|
||||
genFileCopy [relfile|routes/auth/me.js|],
|
||||
genFileCopy [relfile|routes/auth/logout.ts|],
|
||||
genUtils auth,
|
||||
genProvidersIndex auth,
|
||||
genProvidersTypes auth,
|
||||
genFileCopy [relfile|auth/validation.ts|],
|
||||
genFileCopy [relfile|auth/user.ts|]
|
||||
genFileCopy [relfile|auth/user.ts|],
|
||||
genFileCopy [relfile|auth/password.ts|],
|
||||
genFileCopy [relfile|auth/jwt.ts|],
|
||||
genSessionTs auth,
|
||||
genLuciaTs auth
|
||||
]
|
||||
<++> genIndexTs auth
|
||||
<++> genLocalAuth auth
|
||||
@ -69,9 +76,7 @@ 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),
|
||||
"authFieldOnUserEntityName" .= (DbAuth.authFieldOnUserEntityName :: String),
|
||||
"identitiesFieldOnAuthEntityName" .= (DbAuth.identitiesFieldOnAuthEntityName :: String)
|
||||
"userEntityLower" .= (Util.toLowerFirst userEntityName :: String)
|
||||
]
|
||||
|
||||
genAuthRoutesIndex :: AS.Auth.Auth -> Generator FileDraft
|
||||
@ -85,15 +90,6 @@ genAuthRoutesIndex auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Ju
|
||||
authIndexFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||
authIndexFileInSrcDir = [relfile|routes/auth/index.js|]
|
||||
|
||||
genMeRoute :: AS.Auth.Auth -> Generator FileDraft
|
||||
genMeRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
meRouteRelToSrc = [relfile|routes/auth/me.js|]
|
||||
tmplFile = C.asTmplFile $ [reldir|src|] </> meRouteRelToSrc
|
||||
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile meRouteRelToSrc
|
||||
|
||||
tmplData = object ["userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)]
|
||||
|
||||
genUtils :: AS.Auth.Auth -> Generator FileDraft
|
||||
genUtils auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
@ -158,3 +154,38 @@ genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|src/auth/providers
|
||||
userEntityName = AS.refName $ AS.Auth.userEntity auth
|
||||
|
||||
tmplData = object ["userEntityUpper" .= (userEntityName :: String)]
|
||||
|
||||
genLuciaTs :: AS.Auth.Auth -> Generator FileDraft
|
||||
genLuciaTs auth = return $ C.mkTmplFdWithData [relfile|src/auth/lucia.ts|] (Just tmplData)
|
||||
where
|
||||
tmplData =
|
||||
object
|
||||
[ "sessionEntityLower" .= (Util.toLowerFirst DbAuth.sessionEntityName :: String),
|
||||
"authEntityLower" .= (Util.toLowerFirst DbAuth.authEntityName :: String),
|
||||
"userEntityUpper" .= (userEntityName :: String)
|
||||
]
|
||||
|
||||
userEntityName = AS.refName $ AS.Auth.userEntity auth
|
||||
|
||||
genSessionTs :: AS.Auth.Auth -> Generator FileDraft
|
||||
genSessionTs auth = return $ C.mkTmplFdWithData [relfile|src/auth/session.ts|] (Just tmplData)
|
||||
where
|
||||
tmplData =
|
||||
object
|
||||
[ "userEntityUpper" .= userEntityName,
|
||||
"userEntityLower" .= Util.toLowerFirst userEntityName,
|
||||
"authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName,
|
||||
"identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName
|
||||
]
|
||||
|
||||
userEntityName = AS.refName $ AS.Auth.userEntity auth
|
||||
|
||||
depsRequiredByAuth :: AppSpec -> [AS.Dependency.Dependency]
|
||||
depsRequiredByAuth spec = maybe [] (const authDeps) maybeAuth
|
||||
where
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
authDeps =
|
||||
AS.Dependency.fromList
|
||||
[ ("lucia", "^3.0.0-beta.14"),
|
||||
("@lucia-auth/adapter-prisma", "^4.0.0-beta.9")
|
||||
]
|
||||
|
@ -26,10 +26,10 @@ genAuth spec =
|
||||
Nothing -> return []
|
||||
Just auth ->
|
||||
sequence
|
||||
[ genFileCopy [relfile|auth/logout.ts|],
|
||||
genFileCopy [relfile|auth/helpers/user.ts|],
|
||||
[ genFileCopy [relfile|auth/helpers/user.ts|],
|
||||
genFileCopy [relfile|auth/types.ts|],
|
||||
genFileCopy [relfile|auth/user.ts|],
|
||||
genFileCopy [relfile|auth/logout.ts|],
|
||||
genUseAuth auth,
|
||||
genCreateAuthRequiredPage auth
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user