diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/index.ts b/waspc/data/Generator/templates/sdk/wasp/auth/index.ts index 4b79c6ad5e..74109de763 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/index.ts @@ -1,4 +1,8 @@ // PUBLIC -export type { AuthUser } from '../server/_types' - -export { getEmail, getUsername, getFirstProviderUserId, findUserIdentity } from './user.js' +export { + getEmail, + getUsername, + getFirstProviderUserId, + findUserIdentity, + type AuthUser, +} from './user.js' diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/session.ts b/waspc/data/Generator/templates/sdk/wasp/auth/session.ts index 8c2d5e242d..5101ce8caf 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/session.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/session.ts @@ -6,12 +6,10 @@ import { type AuthUser } from 'wasp/auth' import { auth } from "./lucia.js"; import type { Session } from "lucia"; -import { - throwInvalidCredentialsError, - deserializeAndSanitizeProviderData, -} from "./utils.js"; +import { throwInvalidCredentialsError } from "./utils.js"; import { prisma } from 'wasp/server'; +import { createAuthUser } from "./user.js"; // PRIVATE API // Creates a new session for the `authId` in the database @@ -81,29 +79,7 @@ async function getUser(userId: {= userEntityUpper =}['id']): Promise { 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, - }, - } + return createAuthUser(user); } // PRIVATE API diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts index 03d33b5016..2f43b54cc6 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/types.ts @@ -1,2 +1,2 @@ // todo(filip): turn into a proper import/path -export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types' +export type { ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types' diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts b/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts index 1ddbc776f0..07feafcfca 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts @@ -3,7 +3,7 @@ import { deserialize as superjsonDeserialize } from 'superjson' import { useQuery, addMetadataToQuery } from 'wasp/client/operations' import { api, handleApiError } from 'wasp/client/api' import { HttpMethod } from 'wasp/client' -import type { AuthUser } from './types' +import type { AuthUser } from 'wasp/auth' // PUBLIC API export const getMe = createUserGetter() diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts index f9bc6d39a3..9a9944ca61 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/user.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/user.ts @@ -1,17 +1,24 @@ -import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types' +{{={= =}=}} +import { + type {= userEntityName =}, + type {= authEntityName =}, + type {= authIdentityEntityName =}, +} from 'wasp/entities' +import type { ProviderName, DeserializedAuthIdentity } from './types' +import { type PossibleProviderData, deserializeAndSanitizeProviderData } from './utils.js' // PUBLIC API -export function getEmail(user: AuthUser): string | null { +export function getEmail(user: FullUser): string | null { return findUserIdentity(user, "email")?.providerUserId ?? null; } // PUBLIC API -export function getUsername(user: AuthUser): string | null { +export function getUsername(user: FullUser): string | null { return findUserIdentity(user, "username")?.providerUserId ?? null; } // PUBLIC API -export function getFirstProviderUserId(user?: AuthUser): string | null { +export function getFirstProviderUserId(user?: FullUser): string | null { if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) { return null; } @@ -20,8 +27,68 @@ export function getFirstProviderUserId(user?: AuthUser): string | null { } // PUBLIC API -export function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined { +export function findUserIdentity(user: FullUser, providerName: ProviderName): DeserializedAuthIdentity | undefined { return user.auth.identities.find( (identity) => identity.providerName === providerName ); } + +export type AuthUser = ReturnType + +type FullUser = {= userEntityName =} & { + {= authFieldOnUserEntityName =}: FullAuth +} + +type FullAuth = {= authEntityName =} & { + {= identitiesFieldOnAuthEntityName =}: {= authIdentityEntityName =}[] +} + +// PRIVATE API +export function createAuthUser( + user: FullUser +) { + const { auth, ...rest } = user + const identities = { + email: getProviderInfo<'email'>(auth, 'email'), + username: getProviderInfo<'username'>(auth, 'username'), + google: getProviderInfo<'google'>(auth, 'google'), + keycloak: getProviderInfo<'keycloak'>(auth, 'keycloak'), + github: getProviderInfo<'github'>(auth, 'github'), + } + return { + ...rest, + identities, + getFirstProviderUserId: () => getFirstProviderUserId(user), + // Maybe useful for backwards compatibility? Full access? + _rawUser: user, + } +} + +function getProviderInfo( + auth: FullAuth, + providerName: PN +): + | { + id: string + data: PossibleProviderData[PN] + } + | undefined { + const identity = getIdentity(auth, providerName) + if (!identity) { + return undefined + } + return { + id: identity.providerUserId, + data: deserializeAndSanitizeProviderData(identity.providerData, { + shouldRemovePasswordField: true, + }), + } +} + +function getIdentity( + auth: FullAuth, + providerName: ProviderName +): {= authIdentityEntityName =} | undefined { + return auth.{= identitiesFieldOnAuthEntityName =}.find((i) => i.providerName === providerName) +} + diff --git a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts index a1b438e2af..3e2ca2d92c 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts @@ -5,8 +5,6 @@ import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } fr import { prisma } from 'wasp/server' {=# isAuthEnabled =} import { - type {= userEntityName =}, - type {= authEntityName =}, type {= authIdentityEntityName =}, } from "wasp/entities" import { @@ -14,6 +12,7 @@ import { type UsernameProviderData, type OAuthProviderData, } from 'wasp/auth/utils' +import { type AuthUser } from 'wasp/auth' {=/ isAuthEnabled =} import { type _Entity } from "./taggedEntities" import { type Payload } from "./serialization"; @@ -88,20 +87,9 @@ type Context = Expand<{ {=# isAuthEnabled =} type ContextWithUser = Expand & { user?: AuthUser }> -// 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 DeserializedAuthIdentity = Expand & { providerData: Omit | Omit | OAuthProviderData }> -export type AuthUser = {= userEntityName =} & { - {= authFieldOnUserEntityName =}: {= authEntityName =} & { - {= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[] - } | null -} - export type { ProviderName } from 'wasp/auth/utils' {=/ isAuthEnabled =} diff --git a/waspc/examples/todoApp/src/Todo.test.tsx b/waspc/examples/todoApp/src/Todo.test.tsx index 79ec9e5be1..68d0358595 100644 --- a/waspc/examples/todoApp/src/Todo.test.tsx +++ b/waspc/examples/todoApp/src/Todo.test.tsx @@ -1,4 +1,4 @@ -import { type AuthUser as User } from 'wasp/auth' +import { type AuthUser } from 'wasp/auth' import { mockServer, renderInContext } from 'wasp/client/test' import { getTasks, getDate } from 'wasp/client/operations' import { test, expect } from 'vitest' @@ -7,6 +7,7 @@ import { screen } from '@testing-library/react' import Todo, { areThereAnyTasks } from './Todo' import { App } from './App' import { getMe } from 'wasp/client/auth' +import { createAuthUser } from 'wasp/auth/user' test('areThereAnyTasks', () => { expect(areThereAnyTasks([])).toBe(false) @@ -35,7 +36,7 @@ test('handles mock data', async () => { screen.debug() }) -const mockUser = { +const mockUser = createAuthUser({ id: 12, auth: { id: '123', @@ -45,12 +46,12 @@ const mockUser = { authId: '123', providerName: 'email', providerUserId: 'elon@tesla.com', - providerData: '', + providerData: '{}', }, ], }, address: null, -} satisfies User +}) test('handles multiple mock data sources', async () => { mockQuery(getMe, mockUser) diff --git a/waspc/examples/todoApp/src/apis.ts b/waspc/examples/todoApp/src/apis.ts index d744d4912b..2565bdeca5 100644 --- a/waspc/examples/todoApp/src/apis.ts +++ b/waspc/examples/todoApp/src/apis.ts @@ -1,10 +1,9 @@ -import { getFirstProviderUserId } from "wasp/auth"; -import { type MiddlewareConfigFn } from "wasp/server"; -import { type BarBaz, type FooBar, type WebhookCallback } from "wasp/server/api"; +import { type MiddlewareConfigFn } from 'wasp/server' +import { type BarBaz, type FooBar, type WebhookCallback } from 'wasp/server/api' import express from 'express' export const fooBar: FooBar = (_req, res, context) => { - const username = getFirstProviderUserId(context?.user) ?? 'Anonymous' + const username = context.user?.getFirstProviderUserId() res.json({ msg: `Hello, ${username}!` }) } diff --git a/waspc/examples/todoApp/src/dbSeeds.ts b/waspc/examples/todoApp/src/dbSeeds.ts index 849625520b..5958c8b085 100644 --- a/waspc/examples/todoApp/src/dbSeeds.ts +++ b/waspc/examples/todoApp/src/dbSeeds.ts @@ -1,7 +1,8 @@ import { PrismaClient } from '@prisma/client/index.js' -import { type DbSeedFn } from "wasp/server"; +import { type DbSeedFn } from 'wasp/server' import { sanitizeAndSerializeProviderData } from 'wasp/server/auth' import { createTask } from './actions.js' +import { createAuthUser } from 'wasp/auth/user.js' async function createUser(prismaClient: PrismaClient, data: any) { const newUser = await prismaClient.user.create({ @@ -34,14 +35,7 @@ async function createUser(prismaClient: PrismaClient, data: any) { }, }) - if (newUser.auth?.identities) { - newUser.auth.identities = newUser.auth.identities.map((identity) => { - identity.providerData = JSON.parse(identity.providerData) - return identity - }) - } - - return newUser + return createAuthUser(newUser as any) } export const devSeedSimple: DbSeedFn = async (prismaClient) => { diff --git a/waspc/examples/todoApp/src/pages/ProfilePage.tsx b/waspc/examples/todoApp/src/pages/ProfilePage.tsx index f4d10a42c1..230d071e55 100644 --- a/waspc/examples/todoApp/src/pages/ProfilePage.tsx +++ b/waspc/examples/todoApp/src/pages/ProfilePage.tsx @@ -1,16 +1,20 @@ -import { type AuthUser as User } from "wasp/auth"; -import { type ServerToClientPayload, useSocket, useSocketListener } from "wasp/client/webSocket"; -import { Link, routes } from "wasp/client/router"; -import { api } from "wasp/client/api"; +import { type AuthUser } from 'wasp/auth' +import { + type ServerToClientPayload, + useSocket, + useSocketListener, +} from 'wasp/client/webSocket' +import { Link, routes } from 'wasp/client/router' +import { api } from 'wasp/client/api' import React, { useEffect, useRef, useState } from 'react' -import { getName, getProviderData } from '../user' +import { getName } from '../user' async function fetchCustomRoute() { const res = await api.get('/foo/bar') console.log(res.data) } -export const ProfilePage = ({ user }: { user: User }) => { +export const ProfilePage = ({ user }: { user: AuthUser }) => { const [messages, setMessages] = useState< ServerToClientPayload<'chatMessage'>[] >([]) @@ -41,15 +45,13 @@ export const ProfilePage = ({ user }: { user: User }) => { )) const connectionIcon = isConnected ? '🟢' : '🔴' - const providerData = getProviderData(user) - return ( <>

Profile page

Hello {getName(user)}! Your status is{' '} - {providerData && providerData.isEmailVerified + {user.identities.email && user.identities.email.data.isEmailVerified ? 'verfied' : 'unverified'} diff --git a/waspc/examples/todoApp/src/user.ts b/waspc/examples/todoApp/src/user.ts index 55427b52df..5b65de84c2 100644 --- a/waspc/examples/todoApp/src/user.ts +++ b/waspc/examples/todoApp/src/user.ts @@ -1,42 +1,27 @@ -import { getEmail, findUserIdentity, type AuthUser as User } from 'wasp/auth' +import { getEmail, findUserIdentity, type AuthUser } from 'wasp/auth' -export function getName(user?: User) { +export function getName(user?: AuthUser) { if (!user) { return null } // We use multiple auth methods, so we need to check which one is available. - const emailIdentity = findUserIdentity(user, 'email') - if (emailIdentity !== undefined) { - return getEmail(user) + if (user.identities.email !== undefined) { + return user.identities.email.id } - const googleIdentity = findUserIdentity(user, 'google') - if (googleIdentity !== undefined) { - return `Google user ${googleIdentity.providerUserId}` + if (user.identities.google !== undefined) { + return `Google user ${user.identities.google.id}` } - const githubIdentity = findUserIdentity(user, 'github') - if (githubIdentity !== undefined) { - return `GitHub user ${githubIdentity.providerUserId}` + if (user.identities.github !== undefined) { + return `GitHub user ${user.identities.github.id}` } - const keycloakIdentity = findUserIdentity(user, 'keycloak') - if (keycloakIdentity) { - return `Keycloak user ${keycloakIdentity.providerUserId}` + if (user.identities.keycloak !== undefined) { + return `Keycloak user ${user.identities.keycloak.id}` } // If we don't know how to get the name, return null. return null } - -export function getProviderData(user?: User) { - if (!user) { - return null - } - - const emailIdentity = findUserIdentity(user, 'email') - return emailIdentity && 'isEmailVerified' in emailIdentity.providerData - ? emailIdentity.providerData - : null -} diff --git a/waspc/examples/todoApp/src/webSocket.ts b/waspc/examples/todoApp/src/webSocket.ts index 04476f35e2..1643b54267 100644 --- a/waspc/examples/todoApp/src/webSocket.ts +++ b/waspc/examples/todoApp/src/webSocket.ts @@ -1,5 +1,4 @@ -import { getFirstProviderUserId } from "wasp/auth"; -import { type WebSocketDefinition } from "wasp/server/webSocket"; +import { type WebSocketDefinition } from 'wasp/server/webSocket' import { v4 as uuidv4 } from 'uuid' export const webSocketFn: WebSocketDefinition< @@ -8,7 +7,7 @@ export const webSocketFn: WebSocketDefinition< InterServerEvents > = (io, context) => { io.on('connection', (socket) => { - const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown' + const username = socket.data.user?.getFirstProviderUserId() ?? 'Unknown' console.log('a user connected: ', username) socket.on('chatMessage', async (msg) => { diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index b72d391520..4b7d20d973 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -171,11 +171,7 @@ genEntitiesAndServerTypesDirs spec = object [ "entities" .= allEntities, "isAuthEnabled" .= isJust maybeUserEntityName, - "userEntityName" .= userEntityName, - "authEntityName" .= DbAuth.authEntityName, - "authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName, "authIdentityEntityName" .= DbAuth.authIdentityEntityName, - "identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName, "userFieldName" .= toLowerFirst userEntityName ] ) diff --git a/waspc/src/Wasp/Generator/SdkGenerator/AuthG.hs b/waspc/src/Wasp/Generator/SdkGenerator/AuthG.hs index fa2dbc4c8c..c201f9aed0 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/AuthG.hs @@ -29,7 +29,9 @@ genAuth spec = Nothing -> return [] Just auth -> -- shared stuff - sequence [genFileCopy [relfile|auth/user.ts|]] + sequence + [ genUserTs auth + ] -- client stuff <++> sequence [ genFileCopy [relfile|auth/helpers/user.ts|], @@ -131,3 +133,17 @@ genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|auth/providers/typ userEntityName = AS.refName $ AS.Auth.userEntity auth tmplData = object ["userEntityUpper" .= (userEntityName :: String)] + +genUserTs :: AS.Auth.Auth -> Generator FileDraft +genUserTs auth = return $ C.mkTmplFdWithData [relfile|auth/user.ts|] tmplData + where + userEntityName = AS.refName $ AS.Auth.userEntity auth + + tmplData = + object + [ "userEntityName" .= userEntityName, + "authEntityName" .= DbAuth.authEntityName, + "authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName, + "authIdentityEntityName" .= DbAuth.authIdentityEntityName, + "identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName + ]