Skip to content

Commit

Permalink
Update AuthUser API
Browse files Browse the repository at this point in the history
Signed-off-by: Mihovil Ilakovac <[email protected]>
  • Loading branch information
infomiho committed Mar 21, 2024
1 parent 1236648 commit 39f81eb
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 109 deletions.
10 changes: 7 additions & 3 deletions waspc/data/Generator/templates/sdk/wasp/auth/index.ts
Original file line number Diff line number Diff line change
@@ -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'
30 changes: 3 additions & 27 deletions waspc/data/Generator/templates/sdk/wasp/auth/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,29 +79,7 @@ async function getUser(userId: {= userEntityUpper =}['id']): Promise<AuthUser> {
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
Expand Down
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/auth/types.ts
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/wasp/auth/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
77 changes: 72 additions & 5 deletions waspc/data/Generator/templates/sdk/wasp/auth/user.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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<typeof createAuthUser>

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<PN extends ProviderName>(
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<PN>(identity.providerData, {
shouldRemovePasswordField: true,
}),
}
}

function getIdentity(
auth: FullAuth,
providerName: ProviderName
): {= authIdentityEntityName =} | undefined {
return auth.{= identitiesFieldOnAuthEntityName =}.find((i) => i.providerName === providerName)
}

14 changes: 1 addition & 13 deletions waspc/data/Generator/templates/sdk/wasp/server/_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ 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 {
type EmailProviderData,
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";
Expand Down Expand Up @@ -88,20 +87,9 @@ type Context<Entities extends _Entity[]> = Expand<{
{=# isAuthEnabled =}
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { 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<Omit<{= authIdentityEntityName =}, 'providerData'> & {
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
}>

export type AuthUser = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[]
} | null
}

export type { ProviderName } from 'wasp/auth/utils'
{=/ isAuthEnabled =}
9 changes: 5 additions & 4 deletions waspc/examples/todoApp/src/Todo.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -35,7 +36,7 @@ test('handles mock data', async () => {
screen.debug()
})

const mockUser = {
const mockUser = createAuthUser({
id: 12,
auth: {
id: '123',
Expand All @@ -45,12 +46,12 @@ const mockUser = {
authId: '123',
providerName: 'email',
providerUserId: '[email protected]',
providerData: '',
providerData: '{}',
},
],
},
address: null,
} satisfies User
})

test('handles multiple mock data sources', async () => {
mockQuery(getMe, mockUser)
Expand Down
7 changes: 3 additions & 4 deletions waspc/examples/todoApp/src/apis.ts
Original file line number Diff line number Diff line change
@@ -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}!` })
}
Expand Down
12 changes: 3 additions & 9 deletions waspc/examples/todoApp/src/dbSeeds.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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) => {
Expand Down
20 changes: 11 additions & 9 deletions waspc/examples/todoApp/src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -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'>[]
>([])
Expand Down Expand Up @@ -41,15 +45,13 @@ export const ProfilePage = ({ user }: { user: User }) => {
))
const connectionIcon = isConnected ? '🟢' : '🔴'

const providerData = getProviderData(user)

return (
<>
<h2>Profile page</h2>
<div>
Hello <strong>{getName(user)}</strong>! Your status is{' '}
<strong>
{providerData && providerData.isEmailVerified
{user.identities.email && user.identities.email.data.isEmailVerified
? 'verfied'
: 'unverified'}
</strong>
Expand Down
35 changes: 10 additions & 25 deletions waspc/examples/todoApp/src/user.ts
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 2 additions & 3 deletions waspc/examples/todoApp/src/webSocket.ts
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -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) => {
Expand Down
Loading

0 comments on commit 39f81eb

Please sign in to comment.