Skip to content

Commit

Permalink
Implement new wasp/auth API (#1691)
Browse files Browse the repository at this point in the history
  • Loading branch information
infomiho authored Jan 30, 2024
1 parent 3f980bd commit c2abee3
Show file tree
Hide file tree
Showing 27 changed files with 919 additions and 47 deletions.
6 changes: 6 additions & 0 deletions waspc/data/Generator/templates/sdk/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { defineUserSignupFields } from './providers/types.js';

// PUBLIC
export type { AuthUser } from '../server/_types'

// PUBLIC
export { getEmail, getUsername, getFirstProviderUserId, findUserIdentity } from './user.js'
8 changes: 4 additions & 4 deletions waspc/data/Generator/templates/sdk/auth/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Request as ExpressRequest } from "express";

import { type {= userEntityUpper =} } from "wasp/entities"
import { type SanitizedUser } from 'wasp/server/_types'
import { type AuthUser } from 'wasp/auth'

import { auth } from "./lucia.js";
import type { Session } from "lucia";
Expand All @@ -19,7 +19,7 @@ export async function createSession(authId: string): Promise<Session> {
}

export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{
user: SanitizedUser | null,
user: AuthUser | null,
session: Session | null,
}> {
const authorizationHeader = req.headers["authorization"];
Expand All @@ -43,7 +43,7 @@ export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Pro
}

export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{
user: SanitizedUser | null,
user: AuthUser | null,
session: Session | null,
}> {
const { session, user: authEntity } = await auth.validateSession(sessionId);
Expand All @@ -61,7 +61,7 @@ export async function getSessionAndUserFromSessionId(sessionId: string): Promise
}
}

async function getUser(userId: {= userEntityUpper =}['id']): Promise<SanitizedUser> {
async function getUser(userId: {= userEntityUpper =}['id']): Promise<AuthUser> {
const user = await prisma.{= userEntityLower =}
.findUnique({
where: { id: userId },
Expand Down
2 changes: 1 addition & 1 deletion waspc/data/Generator/templates/sdk/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 { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types/'
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/sdk/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 } from 'wasp/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/types'
import type { User } from './types'
import type { AuthUser } from './types'
import { addMetadataToQuery } from 'wasp/rpc/queries'

export const getMe = createUserGetter()
Expand All @@ -15,7 +15,7 @@ export default function useAuth(queryFnArgs?: unknown, config?: any) {
function createUserGetter() {
const getMeRelativePath = 'auth/me'
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
async function getMe(): Promise<User | null> {
async function getMe(): Promise<AuthUser | null> {
try {
const response = await api.get(getMeRoute.path)

Expand Down
10 changes: 5 additions & 5 deletions waspc/data/Generator/templates/sdk/auth/user.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { User, ProviderName, DeserializedAuthIdentity } from './types'
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types'

export function getEmail(user: User): string | null {
export function getEmail(user: AuthUser): string | null {
return findUserIdentity(user, "email")?.providerUserId ?? null;
}

export function getUsername(user: User): string | null {
export function getUsername(user: AuthUser): string | null {
return findUserIdentity(user, "username")?.providerUserId ?? null;
}

export function getFirstProviderUserId(user?: User): string | null {
export function getFirstProviderUserId(user?: AuthUser): string | null {
if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
return null;
}

return user.auth.identities[0].providerUserId ?? null;
}

export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined {
export function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined {
return user.auth.identities.find(
(identity) => identity.providerName === providerName
);
Expand Down
9 changes: 2 additions & 7 deletions waspc/data/Generator/templates/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@
"./rpc/queryClient": "./dist/rpc/queryClient.js",
{=! Used by users, documented. =}
"./types": "./dist/types/index.js",
{=! Used by user, documented. =}
"./auth": "./dist/auth/index.js",
{=! Used by users, documented. =}
"./auth/types": "./dist/auth/types.js",
{=! Used by users, documented. =}
"./auth/login": "./dist/auth/login.js",
{=! Used by users, documented. =}
Expand All @@ -50,8 +46,6 @@
{=! Used by users, documented. =}
"./auth/useAuth": "./dist/auth/useAuth.js",
{=! Used by users, documented. =}
"./auth/user": "./dist/auth/user.js",
{=! Used by users, documented. =}
"./auth/email": "./dist/auth/email/index.js",
{=! Used by our code, uncodumented (but accessible) for users. =}
"./auth/helpers/user": "./dist/auth/helpers/user.js",
Expand Down Expand Up @@ -150,7 +144,8 @@
"./server/api": "./dist/server/api/index.js",
{=! Public: { api } =}
{=! Private: [sdk] =}
"./client/api": "./dist/api/index.js"
"./client/api": "./dist/api/index.js",
"./auth": "./dist/auth/index.js"
},
{=!
TypeScript doesn't care about the redirects we define above in "exports" field; those
Expand Down
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/sdk/server/_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type Context<Entities extends _Entity[]> = Expand<{
}>

{=# isAuthEnabled =}
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { user?: SanitizedUser }>
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,
Expand All @@ -97,7 +97,7 @@ export type DeserializedAuthIdentity = Expand<Omit<{= authIdentityEntityName =},
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
}>

export type SanitizedUser = {= userEntityName =} & {
export type AuthUser = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[]
} | null
Expand Down
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/sdk/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { dirname } from 'path'
import { fileURLToPath } from 'url'

{=# isAuthEnabled =}
import { type SanitizedUser } from 'wasp/server/_types/index.js'
import { type AuthUser } from 'wasp/auth'
{=/ isAuthEnabled =}

type RequestWithExtraFields = Request & {
{=# isAuthEnabled =}
user?: SanitizedUser;
user?: AuthUser;
sessionId?: string;
{=/ isAuthEnabled =}
}
Expand Down
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/sdk/server/webSocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { EventsMap, DefaultEventsMap } from '@socket.io/component-emitter'

import { prisma } from 'wasp/server'
{=# isAuthEnabled =}
import { type SanitizedUser } from 'wasp/server/_types/index.js'
import { type AuthUser } from 'wasp/auth'
{=/ isAuthEnabled =}

{=& userWebSocketFn.importStatement =}
Expand Down Expand Up @@ -33,7 +33,7 @@ export type WebSocketDefinition<

export interface WaspSocketData {
{=# isAuthEnabled =}
user?: SanitizedUser
user?: AuthUser
{=/ isAuthEnabled =}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { handleRejection } from 'wasp/server/utils'
import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js'
{=# isAuthEnabled =}
import auth from 'wasp/core/auth'
import { type SanitizedUser } from 'wasp/server/_types'
import { type AuthUser } from 'wasp/auth'
{=/ isAuthEnabled =}
{=# apiNamespaces =}
Expand Down Expand Up @@ -45,7 +45,7 @@ router.{= routeMethod =}(
{=/ usesAuth =}
handleRejection(
(
req: Parameters<typeof {= importIdentifier =}>[0]{=# usesAuth =} & { user: SanitizedUser }{=/ usesAuth =},
req: Parameters<typeof {= importIdentifier =}>[0]{=# usesAuth =} & { user: AuthUser }{=/ usesAuth =},
res: Parameters<typeof {= importIdentifier =}>[1],
) => {
const context = {
Expand Down
4 changes: 2 additions & 2 deletions waspc/data/Generator/templates/server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { dirname } from 'path'
import { fileURLToPath } from 'url'

{=# isAuthEnabled =}
import { type SanitizedUser } from 'wasp/server/_types/index.js'
import { type AuthUser } from 'wasp/auth'
{=/ isAuthEnabled =}

type RequestWithExtraFields = Request & {
{=# isAuthEnabled =}
user?: SanitizedUser;
user?: AuthUser;
sessionId?: string;
{=/ isAuthEnabled =}
}
Expand Down
108 changes: 108 additions & 0 deletions waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import axios, { type AxiosError } from 'axios'

import config from 'wasp/core/config'
import { storage } from 'wasp/core/storage'
import { apiEventsEmitter } from './events.js'

// PUBLIC API
export const api = axios.create({
baseURL: config.apiUrl,
})

const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId'

let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined

// PRIVATE API (sdk)
export function setSessionId(sessionId: string): void {
waspAppAuthSessionId = sessionId
storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId)
apiEventsEmitter.emit('sessionId.set')
}

// PRIVATE API (sdk)
export function getSessionId(): string | undefined {
return waspAppAuthSessionId
}

// PRIVATE API (sdk)
export function clearSessionId(): void {
waspAppAuthSessionId = undefined
storage.remove(WASP_APP_AUTH_SESSION_ID_NAME)
apiEventsEmitter.emit('sessionId.clear')
}

// PRIVATE API (sdk)
export function removeLocalUserData(): void {
waspAppAuthSessionId = undefined
storage.clear()
apiEventsEmitter.emit('sessionId.clear')
}

api.interceptors.request.use((request) => {
const sessionId = getSessionId()
if (sessionId) {
request.headers['Authorization'] = `Bearer ${sessionId}`
}
return request
})

api.interceptors.response.use(undefined, (error) => {
if (error.response?.status === 401) {
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 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_SESSION_ID_NAME)) {
if (!!event.newValue) {
waspAppAuthSessionId = event.newValue
apiEventsEmitter.emit('sessionId.set')
} else {
waspAppAuthSessionId = undefined
apiEventsEmitter.emit('sessionId.clear')
}
}
})

// PRIVATE API (sdk)
/**
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson)
} else {
// If any other error, we just propagate it.
throw error
}
}

class WaspHttpError extends Error {
statusCode: number

data: unknown

constructor (statusCode: number, message: string, data: unknown) {
super(message)
this.statusCode = statusCode
this.data = data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { setSessionId } from 'wasp/client/api'
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'

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.

// TODO(filip): We are currently removing all the queries, but we should
// remove only non-public, user-dependent queries - public queries are
// expected not to change in respect to the currently logged in user.
await invalidateAndRemoveQueries()
}
17 changes: 17 additions & 0 deletions waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { api, removeLocalUserData } from 'wasp/client/api'
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'

export default async function logout(): Promise<void> {
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// todo(filip): turn into a proper import/path
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
38 changes: 38 additions & 0 deletions waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { deserialize as superjsonDeserialize } from 'superjson'
import { useQuery } from 'wasp/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/types'
import type { AuthUser } from './types'
import { addMetadataToQuery } from 'wasp/rpc/queries'

export const getMe = createUserGetter()

export default function useAuth(queryFnArgs?: unknown, config?: any) {
return useQuery(getMe, queryFnArgs, config)
}

function createUserGetter() {
const getMeRelativePath = 'auth/me'
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
async function getMe(): Promise<AuthUser | null> {
try {
const response = await api.get(getMeRoute.path)

return superjsonDeserialize(response.data)
} catch (error) {
if (error.response?.status === 401) {
return null
} else {
handleApiError(error)
}
}
}

addMetadataToQuery(getMe, {
relativeQueryPath: getMeRelativePath,
queryRoute: getMeRoute,
entitiesUsed: ['User'],
})

return getMe
}
Loading

0 comments on commit c2abee3

Please sign in to comment.