Serialize AuthUser properly (#1995)

This commit is contained in:
Mihovil Ilakovac 2024-05-08 13:47:35 +02:00 committed by GitHub
parent 76580648bc
commit d6796bf7f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 344 additions and 246 deletions

View File

@ -6,6 +6,4 @@ export {
} from './user.js'
// PUBLIC API
export {
type AuthUser,
} from '../server/auth/user.js';
export { type AuthUser } from '../server/auth/user.js'

View File

@ -1,15 +1,15 @@
{{={= =}=}}
import { Request as ExpressRequest } from "express";
import { type {= userEntityUpper =} } from "wasp/entities"
import { type AuthUser } from 'wasp/auth'
import { type {= userEntityUpper =} } from '../entities/index.js';
import { type AuthUserData } from '../server/auth/user.js';
import { auth } from "./lucia.js";
import type { Session } from "lucia";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
import { createAuthUserData } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
@ -17,52 +17,42 @@ export async function createSession(authId: string): Promise<Session> {
return auth.createSession(authId, {});
}
type SessionAndUser = {
session: Session;
user: AuthUserData;
}
// PRIVATE API
export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{
user: AuthUser | null,
session: Session | null,
}> {
export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<SessionAndUser | null> {
const authorizationHeader = req.headers["authorization"];
if (typeof authorizationHeader !== "string") {
return {
user: null,
session: null,
};
return null;
}
const sessionId = auth.readBearerToken(authorizationHeader);
if (!sessionId) {
return {
user: null,
session: null,
};
return null;
}
return getSessionAndUserFromSessionId(sessionId);
}
// PRIVATE API
export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{
user: AuthUser | null,
session: Session | null,
}> {
export async function getSessionAndUserFromSessionId(sessionId: string): Promise<SessionAndUser | null> {
const { session, user: authEntity } = await auth.validateSession(sessionId);
if (!session || !authEntity) {
return {
user: null,
session: null,
};
return null;
}
return {
session,
user: await getUser(authEntity.userId)
user: await getAuthUserData(authEntity.userId)
}
}
async function getUser(userId: {= userEntityUpper =}['id']): Promise<AuthUser> {
async function getAuthUserData(userId: {= userEntityUpper =}['id']): Promise<AuthUserData> {
const user = await prisma.{= userEntityLower =}
.findUnique({
where: { id: userId },
@ -79,7 +69,7 @@ async function getUser(userId: {= userEntityUpper =}['id']): Promise<AuthUser> {
throwInvalidCredentialsError()
}
return createAuthUser(user);
return createAuthUserData(user);
}
// PRIVATE API

View File

@ -4,7 +4,8 @@ import { useQuery, buildAndRegisterQuery } from 'wasp/client/operations'
import type { QueryFunction, Query } from 'wasp/client/operations/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/client'
import type { AuthUser } from '../server/auth/user.js'
import type { AuthUser, AuthUserData } from '../server/auth/user.js'
import { makeAuthUserIfPossible } from '../auth/user.js'
import { UseQueryResult } from '@tanstack/react-query'
// PUBLIC API
@ -20,8 +21,9 @@ function createUserGetter(): Query<void, AuthUser | null> {
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
const getMe: QueryFunction<void, AuthUser | null> = async () => {
try {
const response = await api.get(getMeRoute.path)
return superjsonDeserialize(response.data)
const response = await api.get(getMeRoute.path)
const userData = superjsonDeserialize<AuthUserData | null>(response.data)
return makeAuthUserIfPossible(userData)
} catch (error) {
if (error.response?.status === 401) {
return null

View File

@ -1,6 +1,7 @@
{{={= =}=}}
import { type {= authIdentityEntityName =} } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.js'
import type { AuthUserData, AuthUser } from '../server/auth/user.js'
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
@ -26,6 +27,28 @@ export function getFirstProviderUserId(user?: UserEntityWithAuth): string | null
return user.auth.identities[0].providerUserId ?? null;
}
// PRIVATE API (used in SDK and server)
export type { AuthUserData, AuthUser } from '../server/auth/user.js'
// PRIVATE API (used in SDK and server)
export function makeAuthUserIfPossible(user: null): null
export function makeAuthUserIfPossible(user: AuthUserData): AuthUser
export function makeAuthUserIfPossible(
user: AuthUserData | null,
): AuthUser | null {
return user ? makeAuthUser(user) : null
}
function makeAuthUser(data: AuthUserData): AuthUser {
return {
...data,
getFirstProviderUserId: () => {
const identities = Object.values(data.identities).filter(Boolean);
return identities.length > 0 ? identities[0].id : null;
},
};
}
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): {= authIdentityEntityName =} | null {
if (!user.auth) {
return null;

View File

@ -4,10 +4,10 @@ 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.
@ -16,21 +16,23 @@ import { throwInvalidCredentialsError } from 'wasp/auth/utils'
*/
const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
if (!authHeader) {
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
req.sessionId = null
req.user = null
return next()
}
const { session, user } = await getSessionAndUserFromBearerToken(req);
const sessionAndUser = await getSessionAndUserFromBearerToken(req)
if (!session || !user) {
if (sessionAndUser === null) {
throwInvalidCredentialsError()
}
req.sessionId = session.id
req.user = user
req.sessionId = sessionAndUser.session.id
req.user = sessionAndUser.user
next()
})

View File

@ -22,6 +22,8 @@
{=! Used by our code, uncodumented (but accessible) for users. =}
"./auth/session": "./dist/auth/session.js",
{=! Used by our code, uncodumented (but accessible) for users. =}
"./auth/user": "./dist/auth/user.js",
{=! Used by our code, uncodumented (but accessible) for users. =}
"./auth/providers/types": "./dist/auth/providers/types.js",
{=! Used by our code, uncodumented (but accessible) for users. =}
"./auth/types": "./dist/auth/types.js",

View File

@ -9,23 +9,27 @@ import {
deserializeAndSanitizeProviderData
} from '../../auth/utils.js'
import { type ProviderName } from '../_types/index.js'
import { getFirstProviderUserId } from '../../auth/user.js'
import { Expand } from '../../universal/types.js'
// PUBLIC API
export type AuthUser = AuthUserData & {
getFirstProviderUserId: () => string | null,
}
// PRIVATE API
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* export type AuthUserData = ReturnType<typeof createAuthUserData>
* ```
* to get the benefits of the createAuthUser and the AuthUser type being in sync.
* to get the benefits of the createAuthUser and the AuthUserData type being in sync.
*
* But since we are not using strict mode, the inferred return type of createAuthUser
* is not correct. So we have to define the AuthUser type manually.
* is not correct. So we have to define the AuthUserData type manually.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, '{= authFieldOnUserEntityName =}'> & {
export type AuthUserData = Omit<UserEntityWithAuth, '{= authFieldOnUserEntityName =}'> & {
identities: {
{=# enabledProviders.isEmailAuthEnabled =}
email: Expand<UserFacingProviderData<'email'>> | null
@ -43,7 +47,6 @@ export type AuthUser = Omit<UserEntityWithAuth, '{= authFieldOnUserEntityName =}
github: Expand<UserFacingProviderData<'github'>> | null
{=/ enabledProviders.isGitHubAuthEnabled =}
},
getFirstProviderUserId: () => string | null,
}
type UserFacingProviderData<PN extends ProviderName> = {
@ -61,7 +64,7 @@ export type AuthEntityWithIdentities = {= authEntityName =} & {
}
// PRIVATE API
export function createAuthUser(user: UserEntityWithAuth): AuthUser {
export function createAuthUserData(user: UserEntityWithAuth): AuthUserData {
const { {= authFieldOnUserEntityName =}, ...rest } = user
if (!{= authFieldOnUserEntityName =}) {
throw new Error(`🐝 Error: trying to create a user without auth data.
@ -87,7 +90,6 @@ This should never happen, but it did which means there is a bug in the code.`)
return {
...rest,
identities,
getFirstProviderUserId: () => getFirstProviderUserId(user),
}
}

View File

@ -2,13 +2,13 @@
import { Request, Response, NextFunction } from 'express'
{=# isAuthEnabled =}
import { type AuthUser } from 'wasp/auth'
import { type AuthUserData } from './auth/user.js'
{=/ isAuthEnabled =}
type RequestWithExtraFields = Request & {
{=# isAuthEnabled =}
user?: AuthUser;
sessionId?: string;
user: AuthUserData | null;
sessionId: string | null;
{=/ isAuthEnabled =}
}

View File

@ -39,12 +39,12 @@ export interface WaspSocketData {
{=/ isAuthEnabled =}
}
// PRIVATE API
// PRIVATE API (framework)
export type ServerType = Parameters<WebSocketFn>[0]
// PRIVATE API
// PRIVATE API (sdk)
export type ClientToServerEvents = Events[0]
// PRIVATE API
// PRIVATE API (sdk)
export type ServerToClientEvents = Events[1]
type WebSocketFn = typeof {= userWebSocketFn.importIdentifier =}

View File

@ -4,13 +4,14 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {
const args = (req.body && superjsonDeserialize(req.body)) || {}
const context = {
{=# isAuthEnabled =}
user: req.user
user: makeAuthUserIfPossible(req.user),
{=/ isAuthEnabled =}
}
const result = await handlerFn(args, context)

View File

@ -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 AuthUser } from 'wasp/auth'
import { type AuthUserData, makeAuthUserIfPossible } from 'wasp/auth/user'
{=/ isAuthEnabled =}
{=# apiNamespaces =}
@ -45,12 +45,12 @@ router.{= routeMethod =}(
{=/ usesAuth =}
handleRejection(
(
req: Parameters<typeof {= importIdentifier =}>[0]{=# usesAuth =} & { user: AuthUser }{=/ usesAuth =},
req: Parameters<typeof {= importIdentifier =}>[0]{=# usesAuth =} & { user: AuthUserData | null }{=/ usesAuth =},
res: Parameters<typeof {= importIdentifier =}>[1],
) => {
const context = {
{=# usesAuth =}
user: req.user,
user: makeAuthUserIfPossible(req.user),
{=/ usesAuth =}
entities: {
{=# entities =}

View File

@ -8,6 +8,7 @@ import { config, prisma } from 'wasp/server'
{=# isAuthEnabled =}
import { getSessionAndUserFromSessionId } from 'wasp/auth/session'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
{=/ isAuthEnabled =}
{=& userWebSocketFn.importStatement =}
@ -42,8 +43,12 @@ async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: E
const sessionId = socket.handshake.auth.sessionId
if (sessionId) {
try {
const { user } = await getSessionAndUserFromSessionId(sessionId)
socket.data = { ...socket.data, user }
const sessionAndUser = await getSessionAndUserFromSessionId(sessionId)
const user = sessionAndUser ? makeAuthUserIfPossible(sessionAndUser.user) : null
socket.data = {
...socket.data,
user,
}
} catch (err) { }
}
next()

View File

@ -200,7 +200,7 @@
"file",
"../out/sdk/wasp/package.json"
],
"8df2ebcc130b484aa956ddf0109b23c83fe2221cb41ea35ed5e99817013f36a9"
"995eabb6d01fa0da18e4b83c6686003fc6fcc1b69acc6a6e989281b1b678621a"
],
[
[
@ -445,7 +445,7 @@
"file",
"server/src/middleware/operations.ts"
],
"23efbb9c408f8c12bdb77359a48177430b7da636163562d0105560891ac225b9"
"c165909021613cb6e389b324632f199d392b093d18109f05ab33b720c429c187"
],
[
[

View File

@ -34,6 +34,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -3,6 +3,7 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {

View File

@ -34,6 +34,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -200,7 +200,7 @@
"file",
"../out/sdk/wasp/package.json"
],
"8df2ebcc130b484aa956ddf0109b23c83fe2221cb41ea35ed5e99817013f36a9"
"995eabb6d01fa0da18e4b83c6686003fc6fcc1b69acc6a6e989281b1b678621a"
],
[
[
@ -452,7 +452,7 @@
"file",
"server/src/middleware/operations.ts"
],
"23efbb9c408f8c12bdb77359a48177430b7da636163562d0105560891ac225b9"
"c165909021613cb6e389b324632f199d392b093d18109f05ab33b720c429c187"
],
[
[

View File

@ -34,6 +34,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -3,6 +3,7 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {

View File

@ -447,7 +447,7 @@ waspComplexTest/.wasp/out/server/src/queries/mySpecialQuery.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/auth/me.ts
waspComplexTest/.wasp/out/server/src/routes/crud/index.ts
waspComplexTest/.wasp/out/server/src/routes/crud/tasks.ts
waspComplexTest/.wasp/out/server/src/routes/index.js

View File

@ -95,7 +95,7 @@
"file",
"../out/sdk/wasp/auth/index.ts"
],
"a846aace0af3913411b6c77039cb04d37311d40e5e1f978fd960b38409ec9b66"
"b33a60e857c396669db1839ca155ff0f376f096ec9bb53ddad88168f9a4ed49a"
],
[
[
@ -137,7 +137,7 @@
"file",
"../out/sdk/wasp/auth/session.ts"
],
"03f78291fb369497a07de2f5c2181857e80f158f5eb590b6c095e98c95ff9d14"
"637798c4cd8c130aad2efa254c89269ca31c14db8c3c2fa3efda8892105e00b7"
],
[
[
@ -151,14 +151,14 @@
"file",
"../out/sdk/wasp/auth/useAuth.ts"
],
"a465fececdb235d98c23ce25215c1942bc186c2131e3bb4595d45d9ef219fbd5"
"ebb4fe26cb12321d247c03b136c48742e245de4b6be6886314313498aadf4455"
],
[
[
"file",
"../out/sdk/wasp/auth/user.ts"
],
"2b1911715a41f242cf3f4b8e3954db7146736300fa6888ec8090204eef6b0b6e"
"43743b68e46a4bed06983596968b8d9cd6536d0eb8fb0cbd17100d7f82fc54cd"
],
[
[
@ -347,7 +347,7 @@
"file",
"../out/sdk/wasp/core/auth.ts"
],
"489b2f1840c2d44bdf16f6a949f7217c55c5569a931c3ca9f8484571428840e6"
"2cbfa9369a7fa606f520c27590e86f9ea907f95d41280a327f7e1848279b108f"
],
[
[
@ -473,7 +473,7 @@
"file",
"../out/sdk/wasp/package.json"
],
"75c4ac6e306ef6d0e017dc778ba2c5febe9fea168c3ecf03f80c5086c4e35054"
"97d1ff8002d0f083eb400939f8a916313ff3a90557d17be065b724a2100e5e9c"
],
[
[
@ -522,7 +522,7 @@
"file",
"../out/sdk/wasp/server/auth/user.ts"
],
"76a7add6b2c64e66b583f69320e5cd8ee2595cef709b86d9264cb8d63f7b8adf"
"6c026b896fe346b8e0ccc188d21f86f17f0d482800a793b590df326821c33a7f"
],
[
[
@ -711,7 +711,7 @@
"file",
"../out/sdk/wasp/server/utils.ts"
],
"630dd952c1f4e0c388eb8b73d4f2e9db0cc9015cfc4cb6d6d6dacec6fcd4559c"
"d2e322edb5e1e8bd922b64adb0d6e4a9a3112413224b4da4b1fa7cc50cc3cebb"
],
[
[
@ -963,7 +963,7 @@
"file",
"server/src/middleware/operations.ts"
],
"de21cabd75600314b3b59f4110f27eb5411444ffe26926f2dfaf043932185cae"
"596a0b642427e926b5428cc76192dd4893e88f15b247f6e546f4d24fe29d46a5"
],
[
[
@ -984,7 +984,7 @@
"file",
"server/src/routes/apis/index.ts"
],
"7928725e710e9b7d23ad81ed8d704b314e72fcd731cb17e854ec593ee51f89f5"
"cb1743c9c9c4ce903c5c600eed32bc2b225691b5c59017d640c9b87e2b42d3f8"
],
[
[
@ -1003,7 +1003,7 @@
[
[
"file",
"server/src/routes/auth/me.js"
"server/src/routes/auth/me.ts"
],
"aad9eb2527d58a3d8226ed7ae6c15a09eac7bbb74c58816d01b38fc0a67e2c08"
],

View File

@ -6,6 +6,4 @@ export {
} from './user.js'
// PUBLIC API
export {
type AuthUser,
} from '../server/auth/user.js';
export { type AuthUser } from '../server/auth/user.js'

View File

@ -1,14 +1,14 @@
import { Request as ExpressRequest } from "express";
import { type User } from "wasp/entities"
import { type AuthUser } from 'wasp/auth'
import { type User } from '../entities/index.js';
import { type AuthUserData } from '../server/auth/user.js';
import { auth } from "./lucia.js";
import type { Session } from "lucia";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
import { createAuthUserData } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
@ -16,52 +16,42 @@ export async function createSession(authId: string): Promise<Session> {
return auth.createSession(authId, {});
}
type SessionAndUser = {
session: Session;
user: AuthUserData;
}
// PRIVATE API
export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{
user: AuthUser | null,
session: Session | null,
}> {
export async function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<SessionAndUser | null> {
const authorizationHeader = req.headers["authorization"];
if (typeof authorizationHeader !== "string") {
return {
user: null,
session: null,
};
return null;
}
const sessionId = auth.readBearerToken(authorizationHeader);
if (!sessionId) {
return {
user: null,
session: null,
};
return null;
}
return getSessionAndUserFromSessionId(sessionId);
}
// PRIVATE API
export async function getSessionAndUserFromSessionId(sessionId: string): Promise<{
user: AuthUser | null,
session: Session | null,
}> {
export async function getSessionAndUserFromSessionId(sessionId: string): Promise<SessionAndUser | null> {
const { session, user: authEntity } = await auth.validateSession(sessionId);
if (!session || !authEntity) {
return {
user: null,
session: null,
};
return null;
}
return {
session,
user: await getUser(authEntity.userId)
user: await getAuthUserData(authEntity.userId)
}
}
async function getUser(userId: User['id']): Promise<AuthUser> {
async function getAuthUserData(userId: User['id']): Promise<AuthUserData> {
const user = await prisma.user
.findUnique({
where: { id: userId },
@ -78,7 +68,7 @@ async function getUser(userId: User['id']): Promise<AuthUser> {
throwInvalidCredentialsError()
}
return createAuthUser(user);
return createAuthUserData(user);
}
// PRIVATE API

View File

@ -3,7 +3,8 @@ import { useQuery, buildAndRegisterQuery } from 'wasp/client/operations'
import type { QueryFunction, Query } from 'wasp/client/operations/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/client'
import type { AuthUser } from '../server/auth/user.js'
import type { AuthUser, AuthUserData } from '../server/auth/user.js'
import { makeAuthUserIfPossible } from '../auth/user.js'
import { UseQueryResult } from '@tanstack/react-query'
// PUBLIC API
@ -19,8 +20,9 @@ function createUserGetter(): Query<void, AuthUser | null> {
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
const getMe: QueryFunction<void, AuthUser | null> = async () => {
try {
const response = await api.get(getMeRoute.path)
return superjsonDeserialize(response.data)
const response = await api.get(getMeRoute.path)
const userData = superjsonDeserialize<AuthUserData | null>(response.data)
return makeAuthUserIfPossible(userData)
} catch (error) {
if (error.response?.status === 401) {
return null

View File

@ -1,5 +1,6 @@
import { type AuthIdentity } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.js'
import type { AuthUserData, AuthUser } from '../server/auth/user.js'
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
@ -25,6 +26,28 @@ export function getFirstProviderUserId(user?: UserEntityWithAuth): string | null
return user.auth.identities[0].providerUserId ?? null;
}
// PRIVATE API (used in SDK and server)
export type { AuthUserData, AuthUser } from '../server/auth/user.js'
// PRIVATE API (used in SDK and server)
export function makeAuthUserIfPossible(user: null): null
export function makeAuthUserIfPossible(user: AuthUserData): AuthUser
export function makeAuthUserIfPossible(
user: AuthUserData | null,
): AuthUser | null {
return user ? makeAuthUser(user) : null
}
function makeAuthUser(data: AuthUserData): AuthUser {
return {
...data,
getFirstProviderUserId: () => {
const identities = Object.values(data.identities).filter(Boolean);
return identities.length > 0 ? identities[0].id : null;
},
};
}
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): AuthIdentity | null {
if (!user.auth) {
return null;

View File

@ -4,10 +4,10 @@ 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.
@ -16,21 +16,23 @@ import { throwInvalidCredentialsError } from 'wasp/auth/utils'
*/
const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
if (!authHeader) {
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
req.sessionId = null
req.user = null
return next()
}
const { session, user } = await getSessionAndUserFromBearerToken(req);
const sessionAndUser = await getSessionAndUserFromBearerToken(req)
if (!session || !user) {
if (sessionAndUser === null) {
throwInvalidCredentialsError()
}
req.sessionId = session.id
req.user = user
req.sessionId = sessionAndUser.session.id
req.user = sessionAndUser.user
next()
})

View File

@ -1,2 +1,2 @@
export { getEmail, getUsername, getFirstProviderUserId, } from './user.js';
export { type AuthUser, } from '../server/auth/user.js';
export { type AuthUser } from '../server/auth/user.js';

View File

@ -1,13 +1,12 @@
import { Request as ExpressRequest } from "express";
import { type AuthUser } from 'wasp/auth';
import { type AuthUserData } from '../server/auth/user.js';
import type { Session } from "lucia";
export declare function createSession(authId: string): Promise<Session>;
export declare function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<{
user: AuthUser | null;
session: Session | null;
}>;
export declare function getSessionAndUserFromSessionId(sessionId: string): Promise<{
user: AuthUser | null;
session: Session | null;
}>;
type SessionAndUser = {
session: Session;
user: AuthUserData;
};
export declare function getSessionAndUserFromBearerToken(req: ExpressRequest): Promise<SessionAndUser | null>;
export declare function getSessionAndUserFromSessionId(sessionId: string): Promise<SessionAndUser | null>;
export declare function invalidateSession(sessionId: string): Promise<void>;
export {};

View File

@ -1,7 +1,7 @@
import { auth } from "./lucia.js";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
import { createAuthUserData } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
export async function createSession(authId) {
@ -11,17 +11,11 @@ export async function createSession(authId) {
export async function getSessionAndUserFromBearerToken(req) {
const authorizationHeader = req.headers["authorization"];
if (typeof authorizationHeader !== "string") {
return {
user: null,
session: null,
};
return null;
}
const sessionId = auth.readBearerToken(authorizationHeader);
if (!sessionId) {
return {
user: null,
session: null,
};
return null;
}
return getSessionAndUserFromSessionId(sessionId);
}
@ -29,17 +23,14 @@ export async function getSessionAndUserFromBearerToken(req) {
export async function getSessionAndUserFromSessionId(sessionId) {
const { session, user: authEntity } = await auth.validateSession(sessionId);
if (!session || !authEntity) {
return {
user: null,
session: null,
};
return null;
}
return {
session,
user: await getUser(authEntity.userId)
user: await getAuthUserData(authEntity.userId)
};
}
async function getUser(userId) {
async function getAuthUserData(userId) {
const user = await prisma.user
.findUnique({
where: { id: userId },
@ -54,7 +45,7 @@ async function getUser(userId) {
if (!user) {
throwInvalidCredentialsError();
}
return createAuthUser(user);
return createAuthUserData(user);
}
// PRIVATE API
export function invalidateSession(sessionId) {

View File

@ -1 +1 @@
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../auth/session.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,cAAc;AACd,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc;IAChD,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,cAAc;AACd,MAAM,CAAC,KAAK,UAAU,gCAAgC,CAAC,GAAmB;IAIxE,MAAM,mBAAmB,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEzD,IAAI,OAAO,mBAAmB,KAAK,QAAQ,EAAE,CAAC;QAC5C,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;IAC5D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,OAAO,8BAA8B,CAAC,SAAS,CAAC,CAAC;AACnD,CAAC;AAED,cAAc;AACd,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,SAAiB;IAIpE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IAE5E,IAAI,CAAC,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC;QAC5B,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO;QACP,IAAI,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;KACvC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,MAAkB;IACvC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI;SAC3B,UAAU,CAAC;QACV,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;QACrB,OAAO,EAAE;YACP,IAAI,EAAE;gBACJ,OAAO,EAAE;oBACP,UAAU,EAAE,IAAI;iBACjB;aACF;SACF;KACF,CAAC,CAAA;IAEJ,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,4BAA4B,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,cAAc;AACd,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,OAAO,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC"}
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../auth/session.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,cAAc;AACd,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc;IAChD,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAOD,cAAc;AACd,MAAM,CAAC,KAAK,UAAU,gCAAgC,CAAC,GAAmB;IACxE,MAAM,mBAAmB,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEzD,IAAI,OAAO,mBAAmB,KAAK,QAAQ,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;IAC5D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,8BAA8B,CAAC,SAAS,CAAC,CAAC;AACnD,CAAC;AAED,cAAc;AACd,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,SAAiB;IACpE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IAE5E,IAAI,CAAC,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO;QACP,IAAI,EAAE,MAAM,eAAe,CAAC,UAAU,CAAC,MAAM,CAAC;KAC/C,CAAA;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,MAAkB;IAC/C,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI;SAC3B,UAAU,CAAC;QACV,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;QACrB,OAAO,EAAE;YACP,IAAI,EAAE;gBACJ,OAAO,EAAE;oBACP,UAAU,EAAE,IAAI;iBACjB;aACF;SACF;KACF,CAAC,CAAA;IAEJ,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,4BAA4B,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,cAAc;AACd,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,OAAO,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC"}

View File

@ -2,6 +2,7 @@ import { deserialize as superjsonDeserialize } from 'superjson';
import { useQuery, buildAndRegisterQuery } from 'wasp/client/operations';
import { api, handleApiError } from 'wasp/client/api';
import { HttpMethod } from 'wasp/client';
import { makeAuthUserIfPossible } from '../auth/user.js';
// PUBLIC API
export const getMe = createUserGetter();
// PUBLIC API
@ -15,7 +16,8 @@ function createUserGetter() {
var _a;
try {
const response = await api.get(getMeRoute.path);
return superjsonDeserialize(response.data);
const userData = superjsonDeserialize(response.data);
return makeAuthUserIfPossible(userData);
}
catch (error) {
if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401) {

View File

@ -1 +1 @@
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../auth/useAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAExE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC,aAAa;AACb,MAAM,CAAC,MAAM,KAAK,GAAiC,gBAAgB,EAAE,CAAA;AAErE,aAAa;AACb,MAAM,CAAC,OAAO,UAAU,OAAO;IAC7B,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,gBAAgB;IACvB,MAAM,iBAAiB,GAAG,SAAS,CAAA;IACnC,MAAM,UAAU,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,iBAAiB,EAAE,EAAE,CAAA;IAC5E,MAAM,KAAK,GAAyC,KAAK,IAAI,EAAE;;QAC7D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;YAC/C,OAAO,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAA,MAAA,KAAK,CAAC,QAAQ,0CAAE,MAAM,MAAK,GAAG,EAAE,CAAC;gBACnC,OAAO,IAAI,CAAA;YACb,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,KAAK,CAAC,CAAA;YACvB,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,OAAO,qBAAqB,CAAC,KAAK,EAAE;QAClC,aAAa,EAAE,CAAC,iBAAiB,CAAC;QAClC,UAAU,EAAE,UAAU;QACtB,YAAY,EAAE,CAAC,MAAM,CAAC;KACvB,CAAC,CAAA;AACJ,CAAC"}
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../auth/useAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAExE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAGxD,aAAa;AACb,MAAM,CAAC,MAAM,KAAK,GAAiC,gBAAgB,EAAE,CAAA;AAErE,aAAa;AACb,MAAM,CAAC,OAAO,UAAU,OAAO;IAC7B,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,gBAAgB;IACvB,MAAM,iBAAiB,GAAG,SAAS,CAAA;IACnC,MAAM,UAAU,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,iBAAiB,EAAE,EAAE,CAAA;IAC5E,MAAM,KAAK,GAAyC,KAAK,IAAI,EAAE;;QAC7D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;YAC/C,MAAM,QAAQ,GAAG,oBAAoB,CAAsB,QAAQ,CAAC,IAAI,CAAC,CAAA;YACzE,OAAO,sBAAsB,CAAC,QAAQ,CAAC,CAAA;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAA,MAAA,KAAK,CAAC,QAAQ,0CAAE,MAAM,MAAK,GAAG,EAAE,CAAC;gBACnC,OAAO,IAAI,CAAA;YACb,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,KAAK,CAAC,CAAA;YACvB,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,OAAO,qBAAqB,CAAC,KAAK,EAAE;QAClC,aAAa,EAAE,CAAC,iBAAiB,CAAC;QAClC,UAAU,EAAE,UAAU;QACtB,YAAY,EAAE,CAAC,MAAM,CAAC;KACvB,CAAC,CAAA;AACJ,CAAC"}

View File

@ -1,3 +1,4 @@
import type { AuthUserData, AuthUser } from '../server/auth/user.js';
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
@ -6,3 +7,6 @@ import { type UserEntityWithAuth } from '../server/auth/user.js';
export declare function getEmail(user: UserEntityWithAuth): string | null;
export declare function getUsername(user: UserEntityWithAuth): string | null;
export declare function getFirstProviderUserId(user?: UserEntityWithAuth): string | null;
export type { AuthUserData, AuthUser } from '../server/auth/user.js';
export declare function makeAuthUserIfPossible(user: null): null;
export declare function makeAuthUserIfPossible(user: AuthUserData): AuthUser;

View File

@ -16,6 +16,15 @@ export function getFirstProviderUserId(user) {
}
return (_a = user.auth.identities[0].providerUserId) !== null && _a !== void 0 ? _a : null;
}
export function makeAuthUserIfPossible(user) {
return user ? makeAuthUser(user) : null;
}
function makeAuthUser(data) {
return Object.assign(Object.assign({}, data), { getFirstProviderUserId: () => {
const identities = Object.values(data.identities).filter(Boolean);
return identities.length > 0 ? identities[0].id : null;
} });
}
function findUserIdentity(user, providerName) {
var _a;
if (!user.auth) {

View File

@ -1 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../auth/user.ts"],"names":[],"mappings":"AAQA,aAAa;AACb,MAAM,UAAU,QAAQ,CAAC,IAAwB;;IAC/C,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACjE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,WAAW,CAAC,IAAwB;;IAClD,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACpE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,sBAAsB,CAAC,IAAyB;;IAC9D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,mCAAI,IAAI,CAAC;AACxD,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAwB,EAAE,YAA0B;;IAC5E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAC9B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,KAAK,YAAY,CACrD,mCAAI,IAAI,CAAC;AACZ,CAAC"}
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../auth/user.ts"],"names":[],"mappings":"AASA,aAAa;AACb,MAAM,UAAU,QAAQ,CAAC,IAAwB;;IAC/C,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACjE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,WAAW,CAAC,IAAwB;;IAClD,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACpE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,sBAAsB,CAAC,IAAyB;;IAC9D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,mCAAI,IAAI,CAAC;AACxD,CAAC;AAQD,MAAM,UAAU,sBAAsB,CACpC,IAAyB;IAEzB,OAAO,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,IAAkB;IACtC,uCACK,IAAI,KACP,sBAAsB,EAAE,GAAG,EAAE;YAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClE,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,CAAC,IACD;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAwB,EAAE,YAA0B;;IAC5E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAC9B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,KAAK,YAAY,CACrD,mCAAI,IAAI,CAAC;AACZ,CAAC"}

View File

@ -15,18 +15,20 @@ import { throwInvalidCredentialsError } from 'wasp/auth/utils';
*/
const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization');
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
if (!authHeader) {
// NOTE(matija): for now we let tokenless requests through and make it operation's
// responsibility to verify whether the request is authenticated or not. In the future
// we will develop our own system at Wasp-level for that.
req.sessionId = null;
req.user = null;
return next();
}
const { session, user } = await getSessionAndUserFromBearerToken(req);
if (!session || !user) {
const sessionAndUser = await getSessionAndUserFromBearerToken(req);
if (sessionAndUser === null) {
throwInvalidCredentialsError();
}
req.sessionId = session.id;
req.user = user;
req.sessionId = sessionAndUser.session.id;
req.user = sessionAndUser.user;
next();
});
export default auth;

View File

@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../core/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAE9D;;;;;;;;;;;GAWG;AACH,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACpD,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;IAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,kFAAkF;QAClF,sFAAsF;QACtF,yDAAyD;QACzD,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,gCAAgC,CAAC,GAAG,CAAC,CAAC;IAEtE,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,4BAA4B,EAAE,CAAA;IAChC,CAAC;IAED,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC,EAAE,CAAA;IAC1B,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;IAEf,IAAI,EAAE,CAAA;AACR,CAAC,CAAC,CAAA;AAEF,eAAe,IAAI,CAAA"}
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../core/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAE9D;;;;;;;;;;;GAWG;AACH,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACpD,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;IAC3C,kFAAkF;IAClF,sFAAsF;IACtF,yDAAyD;IACzD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,GAAG,CAAC,SAAS,GAAG,IAAI,CAAA;QACpB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;QACf,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,gCAAgC,CAAC,GAAG,CAAC,CAAA;IAElE,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,4BAA4B,EAAE,CAAA;IAChC,CAAC;IAED,GAAG,CAAC,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,EAAE,CAAA;IACzC,GAAG,CAAC,IAAI,GAAG,cAAc,CAAC,IAAI,CAAA;IAE9B,IAAI,EAAE,CAAA;AACR,CAAC,CAAC,CAAA;AAEF,eAAe,IAAI,CAAA"}

View File

@ -2,23 +2,25 @@ import { type User, type Auth, type AuthIdentity } from '../../entities/index.js
import { type PossibleProviderData } from '../../auth/utils.js';
import { type ProviderName } from '../_types/index.js';
import { Expand } from '../../universal/types.js';
export type AuthUser = AuthUserData & {
getFirstProviderUserId: () => string | null;
};
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* export type AuthUserData = ReturnType<typeof createAuthUserData>
* ```
* to get the benefits of the createAuthUser and the AuthUser type being in sync.
* to get the benefits of the createAuthUser and the AuthUserData type being in sync.
*
* But since we are not using strict mode, the inferred return type of createAuthUser
* is not correct. So we have to define the AuthUser type manually.
* is not correct. So we have to define the AuthUserData type manually.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, 'auth'> & {
export type AuthUserData = Omit<UserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null;
};
getFirstProviderUserId: () => string | null;
};
type UserFacingProviderData<PN extends ProviderName> = {
id: string;
@ -29,5 +31,5 @@ export type UserEntityWithAuth = User & {
export type AuthEntityWithIdentities = Auth & {
identities: AuthIdentity[];
};
export declare function createAuthUser(user: UserEntityWithAuth): AuthUser;
export declare function createAuthUserData(user: UserEntityWithAuth): AuthUserData;
export {};

View File

@ -10,9 +10,8 @@ var __rest = (this && this.__rest) || function (s, e) {
return t;
};
import { deserializeAndSanitizeProviderData } from '../../auth/utils.js';
import { getFirstProviderUserId } from '../../auth/user.js';
// PRIVATE API
export function createAuthUser(user) {
export function createAuthUserData(user) {
const { auth } = user, rest = __rest(user, ["auth"]);
if (!auth) {
throw new Error(`🐝 Error: trying to create a user without auth data.
@ -21,7 +20,7 @@ This should never happen, but it did which means there is a bug in the code.`);
const identities = {
google: getProviderInfo(auth, 'google'),
};
return Object.assign(Object.assign({}, rest), { identities, getFirstProviderUserId: () => getFirstProviderUserId(user) });
return Object.assign(Object.assign({}, rest), { identities });
}
function getProviderInfo(auth, providerName) {
const identity = getIdentity(auth, providerName);

View File

@ -1 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../../server/auth/user.ts"],"names":[],"mappings":";;;;;;;;;;;AAKA,OAAO,EAEL,kCAAkC,EACnC,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAqC3D,cAAc;AACd,MAAM,UAAU,cAAc,CAAC,IAAwB;IACrD,MAAM,EAAE,IAAI,KAAc,IAAI,EAAb,IAAI,UAAK,IAAI,EAAxB,QAAiB,CAAO,CAAA;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC;6EACyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,eAAe,CAAW,IAAI,EAAE,QAAQ,CAAC;KAClD,CAAA;IACD,uCACK,IAAI,KACP,UAAU,EACV,sBAAsB,EAAE,GAAG,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAC3D;AACH,CAAC;AAED,SAAS,eAAe,CACtB,IAA8B,EAC9B,YAAgB;IAIhB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAA;IACb,CAAC;IACD,uCACK,kCAAkC,CAAK,QAAQ,CAAC,YAAY,EAAE;QAC/D,yBAAyB,EAAE,IAAI;KAChC,CAAC,KACF,EAAE,EAAE,QAAQ,CAAC,cAAc,IAC5B;AACH,CAAC;AAED,SAAS,WAAW,CAClB,IAA8B,EAC9B,YAA0B;;IAE1B,OAAO,MAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,mCAAI,IAAI,CAAA;AAC7E,CAAC"}
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../../server/auth/user.ts"],"names":[],"mappings":";;;;;;;;;;;AAKA,OAAO,EAEL,kCAAkC,EACnC,MAAM,qBAAqB,CAAA;AA0C5B,cAAc;AACd,MAAM,UAAU,kBAAkB,CAAC,IAAwB;IACzD,MAAM,EAAE,IAAI,KAAc,IAAI,EAAb,IAAI,UAAK,IAAI,EAAxB,QAAiB,CAAO,CAAA;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC;6EACyD,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,eAAe,CAAW,IAAI,EAAE,QAAQ,CAAC;KAClD,CAAA;IACD,uCACK,IAAI,KACP,UAAU,IACX;AACH,CAAC;AAED,SAAS,eAAe,CACtB,IAA8B,EAC9B,YAAgB;IAIhB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAA;IACb,CAAC;IACD,uCACK,kCAAkC,CAAK,QAAQ,CAAC,YAAY,EAAE;QAC/D,yBAAyB,EAAE,IAAI;KAChC,CAAC,KACF,EAAE,EAAE,QAAQ,CAAC,cAAc,IAC5B;AACH,CAAC;AAED,SAAS,WAAW,CAClB,IAA8B,EAC9B,YAA0B;;IAE1B,OAAO,MAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,mCAAI,IAAI,CAAA;AAC7E,CAAC"}

View File

@ -1,8 +1,8 @@
import { Request, Response, NextFunction } from 'express';
import { type AuthUser } from 'wasp/auth';
import { type AuthUserData } from './auth/user.js';
type RequestWithExtraFields = Request & {
user?: AuthUser;
sessionId?: string;
user: AuthUserData | null;
sessionId: string | null;
};
/**
* Decorator for async express middleware that handles promise rejections.

View File

@ -40,6 +40,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -8,27 +8,30 @@ import {
deserializeAndSanitizeProviderData
} from '../../auth/utils.js'
import { type ProviderName } from '../_types/index.js'
import { getFirstProviderUserId } from '../../auth/user.js'
import { Expand } from '../../universal/types.js'
// PUBLIC API
export type AuthUser = AuthUserData & {
getFirstProviderUserId: () => string | null,
}
// PRIVATE API
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* export type AuthUserData = ReturnType<typeof createAuthUserData>
* ```
* to get the benefits of the createAuthUser and the AuthUser type being in sync.
* to get the benefits of the createAuthUser and the AuthUserData type being in sync.
*
* But since we are not using strict mode, the inferred return type of createAuthUser
* is not correct. So we have to define the AuthUser type manually.
* is not correct. So we have to define the AuthUserData type manually.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, 'auth'> & {
export type AuthUserData = Omit<UserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null
},
getFirstProviderUserId: () => string | null,
}
type UserFacingProviderData<PN extends ProviderName> = {
@ -46,7 +49,7 @@ export type AuthEntityWithIdentities = Auth & {
}
// PRIVATE API
export function createAuthUser(user: UserEntityWithAuth): AuthUser {
export function createAuthUserData(user: UserEntityWithAuth): AuthUserData {
const { auth, ...rest } = user
if (!auth) {
throw new Error(`🐝 Error: trying to create a user without auth data.
@ -58,7 +61,6 @@ This should never happen, but it did which means there is a bug in the code.`)
return {
...rest,
identities,
getFirstProviderUserId: () => getFirstProviderUserId(user),
}
}

View File

@ -1,10 +1,10 @@
import { Request, Response, NextFunction } from 'express'
import { type AuthUser } from 'wasp/auth'
import { type AuthUserData } from './auth/user.js'
type RequestWithExtraFields = Request & {
user?: AuthUser;
sessionId?: string;
user: AuthUserData | null;
sessionId: string | null;
}
/**

View File

@ -3,12 +3,13 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {
const args = (req.body && superjsonDeserialize(req.body)) || {}
const context = {
user: req.user
user: makeAuthUserIfPossible(req.user),
}
const result = await handlerFn(args, context)
const serializedResult = superjsonSerialize(result)

View File

@ -3,7 +3,7 @@ import { prisma } from 'wasp/server'
import { handleRejection } from 'wasp/server/utils'
import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js'
import auth from 'wasp/core/auth'
import { type AuthUser } from 'wasp/auth'
import { type AuthUserData, makeAuthUserIfPossible } from 'wasp/auth/user'
import { fooBarNamespaceMiddlewareFn as _waspfooBarNamespacenamespaceMiddlewareConfigFn } from '../../../../../../src/server/apiNamespaces.js'
@ -25,11 +25,11 @@ router.get(
[auth, ...fooBarMiddleware],
handleRejection(
(
req: Parameters<typeof _waspfooBarfn>[0] & { user: AuthUser },
req: Parameters<typeof _waspfooBarfn>[0] & { user: AuthUserData | null },
res: Parameters<typeof _waspfooBarfn>[1],
) => {
const context = {
user: req.user,
user: makeAuthUserIfPossible(req.user),
entities: {
},
}

View File

@ -207,7 +207,7 @@
"file",
"../out/sdk/wasp/package.json"
],
"a675b7fce46675fe7fc2f5a4edabe8abca4db9cd90a03340d9cf9c54c0e7539f"
"77f20065f4a21450ba6d7e146b4fb0c25a5abb1c616dc7a50723c80021ebe13f"
],
[
[
@ -522,7 +522,7 @@
"file",
"server/src/middleware/operations.ts"
],
"23efbb9c408f8c12bdb77359a48177430b7da636163562d0105560891ac225b9"
"c165909021613cb6e389b324632f199d392b093d18109f05ab33b720c429c187"
],
[
[

View File

@ -35,6 +35,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -3,6 +3,7 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {

View File

@ -200,7 +200,7 @@
"file",
"../out/sdk/wasp/package.json"
],
"8df2ebcc130b484aa956ddf0109b23c83fe2221cb41ea35ed5e99817013f36a9"
"995eabb6d01fa0da18e4b83c6686003fc6fcc1b69acc6a6e989281b1b678621a"
],
[
[
@ -452,7 +452,7 @@
"file",
"server/src/middleware/operations.ts"
],
"23efbb9c408f8c12bdb77359a48177430b7da636163562d0105560891ac225b9"
"c165909021613cb6e389b324632f199d392b093d18109f05ab33b720c429c187"
],
[
[

View File

@ -34,6 +34,7 @@
"./auth/providers/types": "./dist/auth/providers/types.js",
"./auth/session": "./dist/auth/session.js",
"./auth/types": "./dist/auth/types.js",
"./auth/user": "./dist/auth/user.js",
"./auth/utils": "./dist/auth/utils.js",
"./auth/validation": "./dist/auth/validation.js",
"./client": "./dist/client/index.js",

View File

@ -3,6 +3,7 @@ import {
serialize as superjsonSerialize,
} from 'superjson'
import { handleRejection } from 'wasp/server/utils'
import { makeAuthUserIfPossible } from 'wasp/auth/user'
export function createOperation (handlerFn) {
return handleRejection(async (req, res) => {

View File

@ -26,7 +26,7 @@ export const ProfilePage = ({ user }: { user: AuthUser }) => {
}, [])
useSocketListener('chatMessage', (msg) =>
setMessages((priorMessages) => [msg, ...priorMessages])
setMessages((priorMessages) => [msg, ...priorMessages]),
)
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
@ -47,17 +47,18 @@ export const ProfilePage = ({ user }: { user: AuthUser }) => {
return (
<>
<h2>Profile page</h2>
<h2 className="mt-4 mb-2 font-bold text-xl">User Auth Fields Demo</h2>
<div>
Hello <strong>{getName(user)}</strong>! Your status is{' '}
<strong>
{user.identities.email && user.identities.email.isEmailVerified
? 'verfied'
: 'unverified'}
{user.identities.email?.isEmailVerified ? 'verfied' : 'unverified'}
</strong>
.
</div>
<br />
<div>
First provider ID: <strong>{user.getFirstProviderUserId()}</strong>
</div>
<h2 className="mt-4 mb-2 font-bold text-xl">Links Demo</h2>
<Link to="/task/:id" params={{ id: 3 }}>
Task 3
</Link>
@ -70,6 +71,7 @@ export const ProfilePage = ({ user }: { user: AuthUser }) => {
})}
</p>
<div>
<h2 className="mt-4 mb-2 font-bold text-xl">WebSockets Demo</h2>
<form onSubmit={handleSubmit}>
<div className="flex space-x-4 place-items-center">
<div>{connectionIcon}</div>

View File

@ -1,11 +1,11 @@
import { Link } from "wasp/client/router";
import { logout, useAuth } from "wasp/client/auth";
import { useQuery, getDate } from "wasp/client/operations";
import { Link } from 'wasp/client/router'
import { logout, useAuth } from 'wasp/client/auth'
import { useQuery, getDate } from 'wasp/client/operations'
import './Main.css'
import { getName } from './user'
export function App({ children }: any) {
export function App({ children }: { children: React.ReactNode }) {
const { data: user } = useAuth()
const { data: date } = useQuery(getDate)

View File

@ -2,7 +2,6 @@ import React, { useEffect } from 'react'
import { type AuthUser as User } from 'wasp/auth'
import { Link } from 'wasp/client/router'
import { api } from 'wasp/client/api'
import { getName } from '../user'
async function fetchCustomRoute() {
const res = await api.get('/foo/bar')
@ -14,13 +13,11 @@ export const ProfilePage = ({ user }: { user: User }) => {
fetchCustomRoute()
}, [])
const name = getName(user)
return (
<>
<h2>Profile page</h2>
<div>
Hello <strong>{name}</strong>! Your status is{' '}
Hello <strong>{user.getFirstProviderUserId()}</strong>! Your status is{' '}
<strong>
{user.identities.email && user.identities.email.isEmailVerified
? 'verfied'

View File

@ -1,63 +1,62 @@
import { test, expect } from "@playwright/test";
import { test, expect } from '@playwright/test'
test("has title", async ({ page }) => {
await page.goto("http:/localhost:3000");
test('has title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/ToDo App/);
});
test.describe("signup and login", () => {
const randomEmail =
"test" + Math.random().toString(36).substring(7) + "@test.com";
const password = "12345678";
await expect(page).toHaveTitle(/ToDo App/)
})
test.describe('signup and login', () => {
const randomEmail = `test${Math.random().toString(36).substring(7)}@test.com`
const password = '12345678'
test.describe.configure({ mode: "serial" });
test.describe.configure({ mode: 'serial' })
test("social button renders", async ({ page }) => {
await page.goto("/signup");
test('social button renders', async ({ page }) => {
await page.goto('/signup')
await page.waitForSelector("text=Create a new account");
await page.waitForSelector('text=Create a new account')
await expect(
page.locator("a[href='http://localhost:3001/auth/google/login']")
).toBeVisible();
});
page.locator("a[href='http://localhost:3001/auth/google/login']"),
).toBeVisible()
})
test("can sign up", async ({ page }) => {
await page.goto("/signup");
test('can sign up', async ({ page }) => {
await page.goto('/signup')
await page.waitForSelector("text=Create a new account");
await page.waitForSelector('text=Create a new account')
await page.locator("input[type='email']").fill(randomEmail);
await page.locator("input[type='password']").fill(password);
await page.locator("button").click();
await page.locator("input[type='email']").fill(randomEmail)
await page.locator("input[type='password']").fill(password)
await page.locator('button').click()
await expect(page.locator("body")).toContainText(
`You've signed up successfully! Check your email for the confirmation link.`
);
});
await expect(page.locator('body')).toContainText(
`You've signed up successfully! Check your email for the confirmation link.`,
)
})
test("can log in and create a task", async ({ page }) => {
await page.goto("/login");
test('can log in and create a task', async ({ page }) => {
await page.goto('/login')
await page.waitForSelector("text=Log in to your account");
await page.waitForSelector('text=Log in to your account')
await page.locator("input[type='email']").fill(randomEmail);
await page.locator("input[type='password']").fill("12345678xxx");
await page.getByRole("button", { name: "Log in" }).click();
await page.locator("input[type='email']").fill(randomEmail)
await page.locator("input[type='password']").fill('12345678xxx')
await page.getByRole('button', { name: 'Log in' }).click()
await expect(page.locator("body")).toContainText(`Invalid credentials`);
await expect(page.locator('body')).toContainText(`Invalid credentials`)
await page.locator("input[type='password']").fill(password);
await page.getByRole("button", { name: "Log in" }).click();
await page.locator("input[type='password']").fill(password)
await page.getByRole('button', { name: 'Log in' }).click()
await expect(page).toHaveURL("/profile");
await expect(page).toHaveURL('/profile')
await page.goto("/");
await page.goto('/')
const randomTask = "New Task " + Math.random().toString(36).substring(7);
await page.locator("input[type='text']").fill(randomTask);
await page.getByText("Create new task").click();
const randomTask = 'New Task ' + Math.random().toString(36).substring(7)
await page.locator("input[type='text']").fill(randomTask)
await page.getByText('Create new task').click()
await expect(page.locator("body")).toContainText(randomTask);
});
});
await expect(page.locator('body')).toContainText(randomTask)
})
})

View File

@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test'
test.describe('user API', () => {
const randomEmail = `test${Math.random().toString(36).substring(7)}@test.com`
const password = '12345678'
test.describe.configure({ mode: 'serial' })
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage()
// Sign up
await page.goto('/signup')
await page.waitForSelector('text=Create a new account')
await page.locator("input[type='email']").fill(randomEmail)
await page.locator("input[type='password']").fill(password)
await page.locator('button').click()
})
test('user API works on the client', async ({ page }) => {
await page.goto('/login')
await page.waitForSelector('text=Log in to your account')
await page.locator("input[type='email']").fill(randomEmail)
await page.locator("input[type='password']").fill(password)
await page.getByRole('button', { name: 'Log in' }).click()
await page.waitForSelector('text=Profile page')
await expect(page.locator('body')).toContainText(
`Hello ${randomEmail}! Your status is verfied`,
)
await expect(page.locator('a[href="/profile"]')).toContainText(randomEmail)
})
})

View File

@ -46,7 +46,7 @@ genAuth spec = case maybeAuth of
Just auth ->
sequence
[ genAuthRoutesIndex auth,
genFileCopy [relfile|routes/auth/me.js|],
genFileCopy [relfile|routes/auth/me.ts|],
genFileCopy [relfile|routes/auth/logout.ts|],
genProvidersIndex auth
]