mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-08-17 03:00:59 +03:00
Apply latest auth changes to the prototype (#1646)
This commit is contained in:
parent
667a31be86
commit
a35040e351
@ -1,13 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
@ -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>();
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { setAuthToken } from 'wasp/api'
|
||||
import { setSessionId } from 'wasp/api'
|
||||
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'
|
||||
|
||||
export async function initSession(token: string): Promise<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.
|
||||
|
12
waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts
Normal file
12
waspc/data/Generator/templates/sdk/wasp/auth/jwt.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import util from 'util'
|
||||
|
||||
import config from 'wasp/core/config'
|
||||
|
||||
const jwtSign = util.promisify(jwt.sign)
|
||||
const jwtVerify = util.promisify(jwt.verify)
|
||||
|
||||
const JWT_SECRET = config.auth.jwtSecret
|
||||
|
||||
export const signData = (data, options) => jwtSign(data, JWT_SECRET, options)
|
||||
export const verify = (token) => jwtVerify(token, JWT_SECRET)
|
@ -6,7 +6,7 @@ export default async function login(username: string, password: string): Promise
|
||||
const args = { username, password }
|
||||
const response = await api.post('/auth/username/login', args)
|
||||
|
||||
await initSession(response.data.token)
|
||||
await initSession(response.data.sessionId)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { removeLocalUserData } from 'wasp/api'
|
||||
import api, { removeLocalUserData } from 'wasp/api'
|
||||
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'
|
||||
|
||||
export default async function logout(): Promise<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()
|
||||
}
|
||||
}
|
||||
|
55
waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts
Normal file
55
waspc/data/Generator/templates/sdk/wasp/auth/lucia.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Lucia } from "lucia";
|
||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import prisma from '../server/dbClient.js'
|
||||
import config from 'wasp/core/config'
|
||||
import { type User } from "../entities/index.js"
|
||||
|
||||
const prismaAdapter = new PrismaAdapter(
|
||||
// Using `as any` here since Lucia's model types are not compatible with Prisma 4
|
||||
// model types. This is a temporary workaround until we migrate to Prisma 5.
|
||||
// This **works** in runtime, but Typescript complains about it.
|
||||
prisma.session as any,
|
||||
prisma.auth as any
|
||||
);
|
||||
|
||||
/**
|
||||
* We are using Lucia for session management.
|
||||
*
|
||||
* Some details:
|
||||
* 1. We are using the Prisma adapter for Lucia.
|
||||
* 2. We are not using cookies for session management. Instead, we are using
|
||||
* the Authorization header to send the session token.
|
||||
* 3. Our `Session` entity is connected to the `Auth` entity.
|
||||
* 4. We are exposing the `userId` field from the `Auth` entity to
|
||||
* make fetching the User easier.
|
||||
*/
|
||||
export const auth = new Lucia<{}, {
|
||||
userId: User['id']
|
||||
}>(prismaAdapter, {
|
||||
// Since we are not using cookies, we don't need to set any cookie options.
|
||||
// But in the future, if we decide to use cookies, we can set them here.
|
||||
|
||||
// sessionCookie: {
|
||||
// name: "session",
|
||||
// expires: true,
|
||||
// attributes: {
|
||||
// secure: !config.isDevelopment,
|
||||
// sameSite: "lax",
|
||||
// },
|
||||
// },
|
||||
getUserAttributes({ userId }) {
|
||||
return {
|
||||
userId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
declare module "lucia" {
|
||||
interface Register {
|
||||
Lucia: typeof auth;
|
||||
DatabaseSessionAttributes: {};
|
||||
DatabaseUserAttributes: {
|
||||
userId: User['id']
|
||||
};
|
||||
}
|
||||
}
|
15
waspc/data/Generator/templates/sdk/wasp/auth/password.ts
Normal file
15
waspc/data/Generator/templates/sdk/wasp/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.')
|
||||
}
|
||||
}
|
@ -23,16 +23,18 @@ export type InitData = {
|
||||
|
||||
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
|
||||
|
||||
export type PossibleAdditionalSignupFields = Expand<Partial<UserEntityCreateInput>>
|
||||
export type PossibleUserFields = Expand<Partial<UserEntityCreateInput>>
|
||||
|
||||
export function defineAdditionalSignupFields(config: {
|
||||
[key in keyof PossibleAdditionalSignupFields]: FieldGetter<
|
||||
PossibleAdditionalSignupFields[key]
|
||||
export type UserSignupFields = {
|
||||
[key in keyof PossibleUserFields]: FieldGetter<
|
||||
PossibleUserFields[key]
|
||||
>
|
||||
}) {
|
||||
return config
|
||||
}
|
||||
|
||||
type FieldGetter<T> = (
|
||||
data: { [key: string]: unknown }
|
||||
) => Promise<T | undefined> | T | undefined
|
||||
|
||||
export function defineUserSignupFields(fields: UserSignupFields) {
|
||||
return fields
|
||||
}
|
||||
|
107
waspc/data/Generator/templates/sdk/wasp/auth/session.ts
Normal file
107
waspc/data/Generator/templates/sdk/wasp/auth/session.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Request as ExpressRequest } from "express";
|
||||
|
||||
import { type User } from "../entities/index.js"
|
||||
import { type SanitizedUser } from '../server/_types/index.js'
|
||||
|
||||
import { auth } from "./lucia.js";
|
||||
import type { Session } from "lucia";
|
||||
import {
|
||||
throwInvalidCredentialsError,
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from "./utils.js";
|
||||
|
||||
import prisma from '../server/dbClient.js'
|
||||
|
||||
// Creates a new session for the `authId` in the database
|
||||
export async function createSession(authId: string): Promise<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);
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
// todo(filip): turn into a proper import/path
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthEntity } from 'wasp/server/_types/'
|
||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/'
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { hashPassword, sign, verify } from 'wasp/core/auth'
|
||||
import { hashPassword } from './password.js'
|
||||
import { verify } from './jwt.js'
|
||||
import AuthError from '../core/AuthError.js'
|
||||
import HttpError from '../core/HttpError.js'
|
||||
import prisma from '../server/dbClient.js'
|
||||
@ -12,9 +13,7 @@ import { Prisma } from '@prisma/client';
|
||||
|
||||
import { throwValidationError } from './validation.js'
|
||||
|
||||
|
||||
import { defineAdditionalSignupFields, type PossibleAdditionalSignupFields } from './providers/types.js'
|
||||
const _waspAdditionalSignupFieldsConfig = {} as ReturnType<typeof defineAdditionalSignupFields>
|
||||
import { type UserSignupFields, type PossibleUserFields } from './providers/types.js'
|
||||
|
||||
export type EmailProviderData = {
|
||||
hashedPassword: string;
|
||||
@ -127,8 +126,10 @@ export async function findAuthWithUserBy(
|
||||
export async function createUser(
|
||||
providerId: ProviderId,
|
||||
serializedProviderData?: string,
|
||||
userFields?: PossibleAdditionalSignupFields,
|
||||
): Promise<User> {
|
||||
userFields?: PossibleUserFields,
|
||||
): Promise<User & {
|
||||
auth: Auth
|
||||
}> {
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
// Using any here to prevent type errors when userFields are not
|
||||
@ -145,7 +146,12 @@ export async function createUser(
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
// We need to include the Auth entity here because we need `authId`
|
||||
// to be able to create a session.
|
||||
include: {
|
||||
auth: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -155,12 +161,6 @@ export async function deleteUserByAuthId(authId: string): Promise<{ count: numbe
|
||||
} } })
|
||||
}
|
||||
|
||||
export async function createAuthToken(
|
||||
userId: User['id']
|
||||
): Promise<string> {
|
||||
return sign(userId);
|
||||
}
|
||||
|
||||
export async function verifyToken<T = unknown>(token: string): Promise<T> {
|
||||
return verify(token);
|
||||
}
|
||||
@ -224,15 +224,23 @@ export function rethrowPossibleAuthError(e: unknown): void {
|
||||
throw e
|
||||
}
|
||||
|
||||
export async function validateAndGetAdditionalFields(data: {
|
||||
[key: string]: unknown
|
||||
}): Promise<Record<string, any>> {
|
||||
export async function validateAndGetUserFields(
|
||||
data: {
|
||||
[key: string]: unknown
|
||||
},
|
||||
userSignupFields?: UserSignupFields,
|
||||
): Promise<Record<string, any>> {
|
||||
const {
|
||||
password: _password,
|
||||
...sanitizedData
|
||||
} = data;
|
||||
const result: Record<string, any> = {};
|
||||
for (const [field, getFieldValue] of Object.entries(_waspAdditionalSignupFieldsConfig)) {
|
||||
|
||||
if (!userSignupFields) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const [field, getFieldValue] of Object.entries(userSignupFields)) {
|
||||
try {
|
||||
const value = await getFieldValue(sanitizedData)
|
||||
result[field] = value
|
||||
@ -288,3 +296,7 @@ function providerDataHasPasswordField(
|
||||
): providerData is { hashedPassword: string } {
|
||||
return 'hashedPassword' in providerData;
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message?: string): void {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
@ -1,23 +1,22 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import SecurePassword from 'secure-password'
|
||||
import util from 'util'
|
||||
import { randomInt } from 'node:crypto'
|
||||
|
||||
import prisma from '@server/dbClient.js'
|
||||
import { handleRejection } from '../server/utils'
|
||||
import HttpError from './HttpError.js'
|
||||
import config from '../config.js'
|
||||
import { deserializeAndSanitizeProviderData } from 'wasp/auth/utils'
|
||||
|
||||
const jwtSign = util.promisify(jwt.sign)
|
||||
const jwtVerify = util.promisify(jwt.verify)
|
||||
|
||||
const JWT_SECRET = config.auth.jwtSecret
|
||||
|
||||
export const signData = (data, options) => jwtSign(data, JWT_SECRET, options)
|
||||
export const sign = (id, options) => signData({ id }, options)
|
||||
export const verify = (token) => jwtVerify(token, JWT_SECRET)
|
||||
import prisma from '../server/dbClient.js'
|
||||
import { handleRejection } from '../utils.js'
|
||||
import { getSessionAndUserFromBearerToken } from 'wasp/auth/session'
|
||||
import { throwInvalidCredentialsError } from 'wasp/auth/utils'
|
||||
|
||||
/**
|
||||
* Auth middleware
|
||||
*
|
||||
* If the request includes an `Authorization` header it will try to authenticate the request,
|
||||
* otherwise it will let the request through.
|
||||
*
|
||||
* - If authentication succeeds it sets `req.sessionId` and `req.user`
|
||||
* - `req.user` is the user that made the request and it's used in
|
||||
* all Wasp features that need to know the user that made the request.
|
||||
* - `req.sessionId` is the ID of the session that authenticated the request.
|
||||
* - If the request is not authenticated, it throws an error.
|
||||
*/
|
||||
const auth = handleRejection(async (req, res, next) => {
|
||||
const authHeader = req.get('Authorization')
|
||||
if (!authHeader) {
|
||||
@ -27,119 +26,16 @@ const auth = handleRejection(async (req, res, next) => {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7, authHeader.length)
|
||||
req.user = await getUserFromToken(token)
|
||||
} else {
|
||||
const { session, user } = await getSessionAndUserFromBearerToken(req);
|
||||
|
||||
if (!session || !user) {
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
|
||||
req.sessionId = session.id
|
||||
req.user = user
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export async function getUserFromToken(token) {
|
||||
let userIdFromToken
|
||||
try {
|
||||
userIdFromToken = (await verify(token)).id
|
||||
} catch (error) {
|
||||
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
|
||||
throwInvalidCredentialsError()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user
|
||||
.findUnique({
|
||||
where: { id: userIdFromToken },
|
||||
include: {
|
||||
auth: {
|
||||
include: {
|
||||
identities: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!user) {
|
||||
throwInvalidCredentialsError()
|
||||
}
|
||||
|
||||
// TODO: This logic must match the type in types/index.ts (if we remove the
|
||||
// password field from the object here, we must to do the same there).
|
||||
// Ideally, these two things would live in the same place:
|
||||
// https://github.com/wasp-lang/wasp/issues/965
|
||||
let sanitizedUser = { ...user }
|
||||
sanitizedUser.auth.identities = sanitizedUser.auth.identities.map(identity => {
|
||||
identity.providerData = deserializeAndSanitizeProviderData(identity.providerData, { shouldRemovePasswordField: true })
|
||||
return identity
|
||||
});
|
||||
return sanitizedUser
|
||||
}
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
export const hashPassword = async (password) => {
|
||||
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
|
||||
return hashedPwdBuffer.toString("base64")
|
||||
}
|
||||
|
||||
export const verifyPassword = async (hashedPassword, password) => {
|
||||
const result = await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
if (result !== SecurePassword.VALID) {
|
||||
throw new Error('Invalid password.')
|
||||
}
|
||||
}
|
||||
|
||||
// Generates an unused username that looks similar to "quick-purple-sheep-91231".
|
||||
// It generates several options and ensures it picks one that is not currently in use.
|
||||
export function generateAvailableDictionaryUsername() {
|
||||
const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young']
|
||||
const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow']
|
||||
const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep']
|
||||
|
||||
const potentialUsernames = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}`
|
||||
potentialUsernames.push(potentialUsername)
|
||||
}
|
||||
|
||||
return findAvailableUsername(potentialUsernames)
|
||||
}
|
||||
|
||||
// Generates an unused username based on an array of username segments and a separator.
|
||||
// It generates several options and ensures it picks one that is not currently in use.
|
||||
export function generateAvailableUsername(usernameSegments, config) {
|
||||
const separator = config?.separator || '-'
|
||||
const baseUsername = usernameSegments.join(separator)
|
||||
|
||||
const potentialUsernames = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}`
|
||||
potentialUsernames.push(potentialUsername)
|
||||
}
|
||||
|
||||
return findAvailableUsername(potentialUsernames)
|
||||
}
|
||||
|
||||
// Checks the database for an unused username from an array provided and returns first.
|
||||
async function findAvailableUsername(potentialUsernames) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: { in: potentialUsernames },
|
||||
}
|
||||
})
|
||||
const takenUsernames = users.map(user => user.username)
|
||||
const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username))
|
||||
|
||||
if (availableUsernames.length === 0) {
|
||||
throw new Error('Unable to generate a unique username. Please contact Wasp.')
|
||||
}
|
||||
|
||||
return availableUsernames[0]
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message) {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
||||
|
||||
export default auth
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,8 @@ import { fileURLToPath } from 'url'
|
||||
import { type SanitizedUser } from './_types/index.js'
|
||||
|
||||
type RequestWithExtraFields = Request & {
|
||||
user?: SanitizedUser
|
||||
user?: SanitizedUser;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,18 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Task" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"description" TEXT NOT NULL,
|
||||
"isDone" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" INTEGER,
|
||||
CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
@ -30,5 +30,19 @@ CREATE TABLE "AuthIdentity" (
|
||||
CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
Loading…
Reference in New Issue
Block a user