mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-26 22:36:01 +03:00
Implement new wasp/auth API (#1691)
This commit is contained in:
parent
ec9241b780
commit
d650276586
@ -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'
|
||||
|
@ -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";
|
||||
@ -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"];
|
||||
@ -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);
|
||||
@ -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 },
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
@ -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)
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
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;
|
||||
}
|
||||
@ -16,7 +16,7 @@ export function getFirstProviderUserId(user?: User): string | 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
|
||||
);
|
||||
|
@ -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. =}
|
||||
@ -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",
|
||||
@ -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
|
||||
|
@ -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,
|
||||
@ -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
|
||||
|
@ -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 =}
|
||||
}
|
||||
|
@ -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 =}
|
||||
@ -33,7 +33,7 @@ export type WebSocketDefinition<
|
||||
|
||||
export interface WaspSocketData {
|
||||
{=# isAuthEnabled =}
|
||||
user?: SanitizedUser
|
||||
user?: AuthUser
|
||||
{=/ isAuthEnabled =}
|
||||
}
|
||||
|
||||
|
@ -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 =}
|
||||
@ -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 = {
|
||||
|
@ -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 =}
|
||||
}
|
||||
|
108
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts
Normal file
108
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/api/index.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// todo(filip): turn into a proper import/path
|
||||
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
|
@ -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
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types'
|
||||
|
||||
export function getEmail(user: AuthUser): string | null {
|
||||
return findUserIdentity(user, "email")?.providerUserId ?? null;
|
||||
}
|
||||
|
||||
export function getUsername(user: AuthUser): string | null {
|
||||
return findUserIdentity(user, "username")?.providerUserId ?? 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: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined {
|
||||
return user.auth.identities.find(
|
||||
(identity) => identity.providerName === providerName
|
||||
);
|
||||
}
|
302
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts
Normal file
302
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/auth/utils.ts
Normal file
@ -0,0 +1,302 @@
|
||||
import { hashPassword } from './password.js'
|
||||
import { verify } from './jwt.js'
|
||||
import AuthError from 'wasp/core/AuthError'
|
||||
import HttpError from 'wasp/core/HttpError'
|
||||
import { prisma } from 'wasp/server'
|
||||
import { sleep } from 'wasp/server/utils'
|
||||
import {
|
||||
type User,
|
||||
type Auth,
|
||||
type AuthIdentity,
|
||||
} from 'wasp/entities'
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { throwValidationError } from './validation.js'
|
||||
|
||||
import { type UserSignupFields, type PossibleUserFields } from './providers/types.js'
|
||||
|
||||
export type EmailProviderData = {
|
||||
hashedPassword: string;
|
||||
isEmailVerified: boolean;
|
||||
emailVerificationSentAt: string | null;
|
||||
passwordResetSentAt: string | null;
|
||||
}
|
||||
|
||||
export type UsernameProviderData = {
|
||||
hashedPassword: string;
|
||||
}
|
||||
|
||||
export type OAuthProviderData = {}
|
||||
|
||||
/**
|
||||
* This type is used for type-level programming e.g. to enumerate
|
||||
* all possible provider data types.
|
||||
*
|
||||
* The keys of this type are the names of the providers and the values
|
||||
* are the types of the provider data.
|
||||
*/
|
||||
export type PossibleProviderData = {
|
||||
email: EmailProviderData;
|
||||
username: UsernameProviderData;
|
||||
google: OAuthProviderData;
|
||||
github: OAuthProviderData;
|
||||
}
|
||||
|
||||
export type ProviderName = keyof PossibleProviderData
|
||||
|
||||
export const contextWithUserEntity = {
|
||||
entities: {
|
||||
User: prisma.user
|
||||
}
|
||||
}
|
||||
|
||||
export const authConfig = {
|
||||
failureRedirectPath: "/login",
|
||||
successRedirectPath: "/",
|
||||
}
|
||||
|
||||
/**
|
||||
* ProviderId uniquely identifies an auth identity e.g.
|
||||
* "email" provider with user id "test@test.com" or
|
||||
* "google" provider with user id "1234567890".
|
||||
*
|
||||
* We use this type to avoid passing the providerName and providerUserId
|
||||
* separately. Also, we can normalize the providerUserId to make sure it's
|
||||
* consistent across different DB operations.
|
||||
*/
|
||||
export type ProviderId = {
|
||||
providerName: ProviderName;
|
||||
providerUserId: string;
|
||||
}
|
||||
|
||||
export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId {
|
||||
return {
|
||||
providerName,
|
||||
providerUserId: providerUserId.toLowerCase(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function findAuthIdentity(providerId: ProviderId): Promise<AuthIdentity | null> {
|
||||
return prisma.authIdentity.findUnique({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the provider data for the given auth identity.
|
||||
*
|
||||
* This function performs data sanitization and serialization.
|
||||
* Sanitization is done by hashing the password, so this function
|
||||
* expects the password received in the `providerDataUpdates`
|
||||
* **not to be hashed**.
|
||||
*/
|
||||
export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
||||
providerId: ProviderId,
|
||||
existingProviderData: PossibleProviderData[PN],
|
||||
providerDataUpdates: Partial<PossibleProviderData[PN]>,
|
||||
): Promise<AuthIdentity> {
|
||||
// We are doing the sanitization here only on updates to avoid
|
||||
// hashing the password multiple times.
|
||||
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
|
||||
const newProviderData = {
|
||||
...existingProviderData,
|
||||
...sanitizedProviderDataUpdates,
|
||||
}
|
||||
const serializedProviderData = await serializeProviderData<PN>(newProviderData);
|
||||
return prisma.authIdentity.update({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
},
|
||||
data: { providerData: serializedProviderData },
|
||||
});
|
||||
}
|
||||
|
||||
type FindAuthWithUserResult = Auth & {
|
||||
user: User
|
||||
}
|
||||
|
||||
export async function findAuthWithUserBy(
|
||||
where: Prisma.AuthWhereInput
|
||||
): Promise<FindAuthWithUserResult> {
|
||||
return prisma.auth.findFirst({ where, include: { user: true }});
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
providerId: ProviderId,
|
||||
serializedProviderData?: string,
|
||||
userFields?: PossibleUserFields,
|
||||
): Promise<User & {
|
||||
auth: Auth
|
||||
}> {
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
// Using any here to prevent type errors when userFields are not
|
||||
// defined. We want Prisma to throw an error in that case.
|
||||
...(userFields ?? {} as any),
|
||||
auth: {
|
||||
create: {
|
||||
identities: {
|
||||
create: {
|
||||
providerName: providerId.providerName,
|
||||
providerUserId: providerId.providerUserId,
|
||||
providerData: serializedProviderData,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
// We need to include the Auth entity here because we need `authId`
|
||||
// to be able to create a session.
|
||||
include: {
|
||||
auth: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
|
||||
return prisma.user.deleteMany({ where: { auth: {
|
||||
id: authId,
|
||||
} } })
|
||||
}
|
||||
|
||||
export async function verifyToken<T = unknown>(token: string): Promise<T> {
|
||||
return verify(token);
|
||||
}
|
||||
|
||||
// If an user exists, we don't want to leak information
|
||||
// about it. Pretending that we're doing some work
|
||||
// will make it harder for an attacker to determine
|
||||
// if a user exists or not.
|
||||
// NOTE: Attacker measuring time to response can still determine
|
||||
// if a user exists or not. We'll be able to avoid it when
|
||||
// we implement e-mail sending via jobs.
|
||||
export async function doFakeWork(): Promise<unknown> {
|
||||
const timeToWork = Math.floor(Math.random() * 1000) + 1000;
|
||||
return sleep(timeToWork);
|
||||
}
|
||||
|
||||
export function rethrowPossibleAuthError(e: unknown): void {
|
||||
if (e instanceof AuthError) {
|
||||
throwValidationError(e.message);
|
||||
}
|
||||
|
||||
// Prisma code P2002 is for unique constraint violations.
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new HttpError(422, 'Save failed', {
|
||||
message: `user with the same identity already exists`,
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof Prisma.PrismaClientValidationError) {
|
||||
// NOTE: Logging the error since this usually means that there are
|
||||
// required fields missing in the request, we want the developer
|
||||
// to know about it.
|
||||
console.error(e)
|
||||
throw new HttpError(422, 'Save failed', {
|
||||
message: 'there was a database error'
|
||||
})
|
||||
}
|
||||
|
||||
// Prisma code P2021 is for missing table errors.
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') {
|
||||
// NOTE: Logging the error since this usually means that the database
|
||||
// migrations weren't run, we want the developer to know about it.
|
||||
console.error(e)
|
||||
console.info('🐝 This error can happen if you did\'t run the database migrations.')
|
||||
throw new HttpError(500, 'Save failed', {
|
||||
message: `there was a database error`,
|
||||
})
|
||||
}
|
||||
|
||||
// Prisma code P2003 is for foreign key constraint failure
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
|
||||
console.error(e)
|
||||
console.info(`🐝 This error can happen if you have some relation on your User entity
|
||||
but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
|
||||
Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
|
||||
throw new HttpError(500, 'Save failed', {
|
||||
message: `there was a database error`,
|
||||
})
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
export async function validateAndGetUserFields(
|
||||
data: {
|
||||
[key: string]: unknown
|
||||
},
|
||||
userSignupFields?: UserSignupFields,
|
||||
): Promise<Record<string, any>> {
|
||||
const {
|
||||
password: _password,
|
||||
...sanitizedData
|
||||
} = data;
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
if (!userSignupFields) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const [field, getFieldValue] of Object.entries(userSignupFields)) {
|
||||
try {
|
||||
const value = await getFieldValue(sanitizedData)
|
||||
result[field] = value
|
||||
} catch (e) {
|
||||
throwValidationError(e.message)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function deserializeAndSanitizeProviderData<PN extends ProviderName>(
|
||||
providerData: string,
|
||||
{ shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {},
|
||||
): PossibleProviderData[PN] {
|
||||
// NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON.
|
||||
let data = JSON.parse(providerData) as PossibleProviderData[PN];
|
||||
|
||||
if (providerDataHasPasswordField(data) && shouldRemovePasswordField) {
|
||||
delete data.hashedPassword;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function sanitizeAndSerializeProviderData<PN extends ProviderName>(
|
||||
providerData: PossibleProviderData[PN],
|
||||
): Promise<string> {
|
||||
return serializeProviderData(
|
||||
await sanitizeProviderData(providerData)
|
||||
);
|
||||
}
|
||||
|
||||
function serializeProviderData<PN extends ProviderName>(providerData: PossibleProviderData[PN]): string {
|
||||
return JSON.stringify(providerData);
|
||||
}
|
||||
|
||||
async function sanitizeProviderData<PN extends ProviderName>(
|
||||
providerData: PossibleProviderData[PN],
|
||||
): Promise<PossibleProviderData[PN]> {
|
||||
const data = {
|
||||
...providerData,
|
||||
};
|
||||
if (providerDataHasPasswordField(data)) {
|
||||
data.hashedPassword = await hashPassword(data.hashedPassword);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function providerDataHasPasswordField(
|
||||
providerData: PossibleProviderData[keyof PossibleProviderData],
|
||||
): providerData is { hashedPassword: string } {
|
||||
return 'hashedPassword' in providerData;
|
||||
}
|
||||
|
||||
export function throwInvalidCredentialsError(message?: string): void {
|
||||
throw new HttpError(401, 'Invalid credentials', { message })
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { api, handleApiError } from 'wasp/client/api'
|
||||
import { HttpMethod } from 'wasp/types'
|
||||
import {
|
||||
serialize as superjsonSerialize,
|
||||
deserialize as superjsonDeserialize,
|
||||
} from 'superjson'
|
||||
|
||||
export type OperationRoute = { method: HttpMethod, path: string }
|
||||
|
||||
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
|
||||
try {
|
||||
const superjsonArgs = superjsonSerialize(args)
|
||||
const response = await api.post(operationRoute.path, superjsonArgs)
|
||||
return superjsonDeserialize(response.data)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
|
||||
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
|
||||
}
|
116
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json
Normal file
116
waspc/examples/todo-typescript/.wasp/out/sdk/wasp/package.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"name": "wasp",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist"
|
||||
},
|
||||
"exports": {
|
||||
"./core/HttpError": "./dist/core/HttpError.js",
|
||||
"./core/AuthError": "./dist/core/AuthError.js",
|
||||
"./core/config": "./dist/core/config.js",
|
||||
"./core/stitches.config": "./dist/core/stitches.config.js",
|
||||
"./core/storage": "./dist/core/storage.js",
|
||||
"./core/auth": "./dist/core/auth.js",
|
||||
"./rpc": "./dist/rpc/index.js",
|
||||
"./rpc/queries": "./dist/rpc/queries/index.js",
|
||||
"./rpc/queries/core": "./dist/rpc/queries/core.js",
|
||||
"./rpc/actions": "./dist/rpc/actions/index.js",
|
||||
"./rpc/actions/core": "./dist/rpc/actions/core.js",
|
||||
"./rpc/queryClient": "./dist/rpc/queryClient.js",
|
||||
"./types": "./dist/types/index.js",
|
||||
"./auth/login": "./dist/auth/login.js",
|
||||
"./auth/logout": "./dist/auth/logout.js",
|
||||
"./auth/signup": "./dist/auth/signup.js",
|
||||
"./auth/useAuth": "./dist/auth/useAuth.js",
|
||||
"./auth/email": "./dist/auth/email/index.js",
|
||||
"./auth/helpers/user": "./dist/auth/helpers/user.js",
|
||||
"./auth/session": "./dist/auth/session.js",
|
||||
"./auth/providers/types": "./dist/auth/providers/types.js",
|
||||
"./auth/utils": "./dist/auth/utils.js",
|
||||
"./auth/password": "./dist/auth/password.js",
|
||||
"./auth/jwt": "./dist/auth/jwt.js",
|
||||
"./auth/validation": "./dist/auth/validation.js",
|
||||
"./auth/forms/Login": "./dist/auth/forms/Login.jsx",
|
||||
"./auth/forms/Signup": "./dist/auth/forms/Signup.jsx",
|
||||
"./auth/forms/VerifyEmail": "./dist/auth/forms/VerifyEmail.jsx",
|
||||
"./auth/forms/ForgotPassword": "./dist/auth/forms/ForgotPassword.jsx",
|
||||
"./auth/forms/ResetPassword": "./dist/auth/forms/ResetPassword.jsx",
|
||||
"./auth/forms/internal/Form": "./dist/auth/forms/internal/Form.jsx",
|
||||
"./auth/helpers/*": "./dist/auth/helpers/*.jsx",
|
||||
"./auth/pages/createAuthRequiredPage": "./dist/auth/pages/createAuthRequiredPage.jsx",
|
||||
"./api/events": "./dist/api/events.js",
|
||||
"./operations": "./dist/operations/index.js",
|
||||
"./ext-src/*": "./dist/ext-src/*.js",
|
||||
"./operations/*": "./dist/operations/*",
|
||||
"./universal/url": "./dist/universal/url.js",
|
||||
"./universal/types": "./dist/universal/types.js",
|
||||
"./universal/validators": "./dist/universal/validators.js",
|
||||
"./server/middleware": "./dist/server/middleware/index.js",
|
||||
"./server/utils": "./dist/server/utils.js",
|
||||
"./server/actions": "./dist/server/actions/index.js",
|
||||
"./server/queries": "./dist/server/queries/index.js",
|
||||
"./server/auth/email": "./dist/server/auth/email/index.js",
|
||||
"./dbSeed/types": "./dist/dbSeed/types.js",
|
||||
"./test": "./dist/test/index.js",
|
||||
"./test/*": "./dist/test/*.js",
|
||||
"./crud/*": "./dist/crud/*.js",
|
||||
"./server/crud/*": "./dist/server/crud/*",
|
||||
"./email": "./dist/email/index.js",
|
||||
"./email/core/types": "./dist/email/core/types.js",
|
||||
"./server/auth/email/utils": "./dist/server/auth/email/utils.js",
|
||||
"./jobs/*": "./dist/jobs/*.js",
|
||||
"./jobs/pgBoss/types": "./dist/jobs/pgBoss/types.js",
|
||||
"./router": "./dist/router/index.js",
|
||||
"./server/webSocket": "./dist/server/webSocket/index.js",
|
||||
"./webSocket": "./dist/webSocket/index.js",
|
||||
"./webSocket/WebSocketProvider": "./dist/webSocket/WebSocketProvider.jsx",
|
||||
|
||||
"./server/types": "./dist/server/types/index.js",
|
||||
|
||||
"./server": "./dist/server/index.js",
|
||||
"./server/api": "./dist/server/api/index.js",
|
||||
"./client/api": "./dist/api/index.js",
|
||||
"./auth": "./dist/auth/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"client/api": ["api/index.ts"]
|
||||
}
|
||||
},
|
||||
"license": "ISC",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"dependencies": {"@prisma/client": "4.16.2",
|
||||
"prisma": "4.16.2",
|
||||
"@tanstack/react-query": "^4.29.0",
|
||||
"axios": "^1.4.0",
|
||||
"express": "~4.18.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mitt": "3.0.0",
|
||||
"react": "^18.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"secure-password": "^4.0.0",
|
||||
"superjson": "^1.12.2",
|
||||
"@types/express-serve-static-core": "^4.17.13",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"lucia": "^3.0.0-beta.14",
|
||||
"@lucia-auth/adapter-prisma": "^4.0.0-beta.9",
|
||||
"socket.io": "^4.6.1",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"@socket.io/component-emitter": "^4.0.0",
|
||||
"vitest": "^1.2.1",
|
||||
"@vitest/ui": "^1.2.1",
|
||||
"jsdom": "^21.1.1",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/jest-dom": "^6.3.0",
|
||||
"msw": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {"@tsconfig/node18": "latest"
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import { type Expand } from 'wasp/universal/types';
|
||||
import { type Request, type Response } from 'express'
|
||||
import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core'
|
||||
import { prisma } from 'wasp/server'
|
||||
import {
|
||||
type User,
|
||||
type Auth,
|
||||
type AuthIdentity,
|
||||
} from "wasp/entities"
|
||||
import {
|
||||
type EmailProviderData,
|
||||
type UsernameProviderData,
|
||||
type OAuthProviderData,
|
||||
} from 'wasp/auth/utils'
|
||||
import { type _Entity } from "./taggedEntities"
|
||||
import { type Payload } from "./serialization";
|
||||
|
||||
export * from "./taggedEntities"
|
||||
export * from "./serialization"
|
||||
|
||||
export type Query<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
|
||||
Operation<Entities, Input, Output>
|
||||
|
||||
export type Action<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
|
||||
Operation<Entities, Input, Output>
|
||||
|
||||
export type AuthenticatedQuery<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
|
||||
AuthenticatedOperation<Entities, Input, Output>
|
||||
|
||||
export type AuthenticatedAction<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
|
||||
AuthenticatedOperation<Entities, Input, Output>
|
||||
|
||||
type AuthenticatedOperation<Entities extends _Entity[], Input extends Payload, Output extends Payload> = (
|
||||
args: Input,
|
||||
context: ContextWithUser<Entities>,
|
||||
) => Output | Promise<Output>
|
||||
|
||||
export type AuthenticatedApi<
|
||||
Entities extends _Entity[],
|
||||
Params extends ExpressParams,
|
||||
ResBody,
|
||||
ReqBody,
|
||||
ReqQuery extends ExpressQuery,
|
||||
Locals extends Record<string, any>
|
||||
> = (
|
||||
req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>,
|
||||
res: Response<ResBody, Locals>,
|
||||
context: ContextWithUser<Entities>,
|
||||
) => void
|
||||
|
||||
type Operation<Entities extends _Entity[], Input, Output> = (
|
||||
args: Input,
|
||||
context: Context<Entities>,
|
||||
) => Output | Promise<Output>
|
||||
|
||||
export type Api<
|
||||
Entities extends _Entity[],
|
||||
Params extends ExpressParams,
|
||||
ResBody,
|
||||
ReqBody,
|
||||
ReqQuery extends ExpressQuery,
|
||||
Locals extends Record<string, any>
|
||||
> = (
|
||||
req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>,
|
||||
res: Response<ResBody, Locals>,
|
||||
context: Context<Entities>,
|
||||
) => void
|
||||
|
||||
type EntityMap<Entities extends _Entity[]> = {
|
||||
[EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName]
|
||||
}
|
||||
|
||||
export type PrismaDelegate = {
|
||||
"User": typeof prisma.user,
|
||||
"Task": typeof prisma.task,
|
||||
}
|
||||
|
||||
type Context<Entities extends _Entity[]> = Expand<{
|
||||
entities: Expand<EntityMap<Entities>>
|
||||
}>
|
||||
|
||||
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<AuthIdentity, 'providerData'> & {
|
||||
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
|
||||
}>
|
||||
|
||||
export type AuthUser = User & {
|
||||
auth: Auth & {
|
||||
identities: DeserializedAuthIdentity[]
|
||||
} | null
|
||||
}
|
||||
|
||||
export type { ProviderName } from 'wasp/auth/utils'
|
@ -0,0 +1,50 @@
|
||||
import { prisma } from 'wasp/server'
|
||||
|
||||
import { createTask as createTask_ext } from 'wasp/ext-src/task/actions.js'
|
||||
import { updateTask as updateTask_ext } from 'wasp/ext-src/task/actions.js'
|
||||
import { deleteTasks as deleteTasks_ext } from 'wasp/ext-src/task/actions.js'
|
||||
import { send as send_ext } from 'wasp/ext-src/user/customEmailSending.js'
|
||||
|
||||
export type CreateTask = typeof createTask_ext
|
||||
|
||||
export const createTask = async (args, context) => {
|
||||
return (createTask_ext as any)(args, {
|
||||
...context,
|
||||
entities: {
|
||||
Task: prisma.task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export type UpdateTask = typeof updateTask_ext
|
||||
|
||||
export const updateTask = async (args, context) => {
|
||||
return (updateTask_ext as any)(args, {
|
||||
...context,
|
||||
entities: {
|
||||
Task: prisma.task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export type DeleteTasks = typeof deleteTasks_ext
|
||||
|
||||
export const deleteTasks = async (args, context) => {
|
||||
return (deleteTasks_ext as any)(args, {
|
||||
...context,
|
||||
entities: {
|
||||
Task: prisma.task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export type CustomEmailSending = typeof send_ext
|
||||
|
||||
export const customEmailSending = async (args, context) => {
|
||||
return (send_ext as any)(args, {
|
||||
...context,
|
||||
entities: {
|
||||
User: prisma.user,
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { prisma } from 'wasp/server'
|
||||
|
||||
import { getTasks as getTasks_ext } from 'wasp/ext-src/task/queries.js'
|
||||
|
||||
export type GetTasks = typeof getTasks_ext
|
||||
|
||||
export const getTasks = async (args, context) => {
|
||||
return (getTasks_ext as any)(args, {
|
||||
...context,
|
||||
entities: {
|
||||
Task: prisma.task,
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import crypto from 'crypto'
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
|
||||
import { readdir } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { type AuthUser } from 'wasp/auth'
|
||||
|
||||
type RequestWithExtraFields = Request & {
|
||||
user?: AuthUser;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for async express middleware that handles promise rejections.
|
||||
* @param {Func} middleware - Express middleware function.
|
||||
* @returns Express middleware that is exactly the same as the given middleware but,
|
||||
* if given middleware returns promise, reject of that promise will be correctly handled,
|
||||
* meaning that error will be forwarded to next().
|
||||
*/
|
||||
export const handleRejection = (
|
||||
middleware: (
|
||||
req: RequestWithExtraFields,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => any
|
||||
) =>
|
||||
async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await middleware(req, res, next)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const sleep = (ms: number): Promise<unknown> => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
export function getDirPathFromFileUrl(fileUrl: string): string {
|
||||
return fileURLToPath(dirname(fileUrl))
|
||||
}
|
||||
|
||||
export async function importJsFilesFromDir(
|
||||
pathToDir: string,
|
||||
whitelistedFileNames: string[] | null = null
|
||||
): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
readdir(pathToDir, async (err, files) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
const importPromises = files
|
||||
.filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
|
||||
.map((file) => import(`${pathToDir}/${file}`))
|
||||
resolve(Promise.all(importPromises))
|
||||
})
|
||||
})
|
||||
|
||||
function isWhitelistedFileName(fileName: string) {
|
||||
// No whitelist means all files are whitelisted
|
||||
if (!Array.isArray(whitelistedFileNames)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
|
||||
}
|
||||
}
|
@ -11,8 +11,7 @@ import {
|
||||
} from 'wasp/rpc/actions'
|
||||
import waspLogo from './waspLogo.png'
|
||||
import type { Task } from 'wasp/entities'
|
||||
import type { User } from 'wasp/auth/types'
|
||||
import { getFirstProviderUserId } from 'wasp/auth/user'
|
||||
import { AuthUser, getFirstProviderUserId } from 'wasp/auth'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Tasks } from 'wasp/crud/Tasks'
|
||||
// import login from 'wasp/auth/login'
|
||||
@ -20,7 +19,7 @@ import { Tasks } from 'wasp/crud/Tasks'
|
||||
import useAuth from 'wasp/auth/useAuth'
|
||||
import { Todo } from './Todo'
|
||||
|
||||
export const MainPage = ({ user }: { user: User }) => {
|
||||
export const MainPage = ({ user }: { user: AuthUser }) => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
const { data: userAgain } = useAuth()
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { mockServer, renderInContext } from 'wasp/test'
|
||||
import { getTasks } from 'wasp/rpc/queries'
|
||||
import { Todo, areThereAnyTasks } from './Todo'
|
||||
import { MainPage } from './MainPage'
|
||||
import type { User } from 'wasp/auth/types'
|
||||
import type { AuthUser } from 'wasp/auth'
|
||||
import { getMe } from 'wasp/auth/useAuth'
|
||||
import { Tasks } from 'wasp/crud/Tasks'
|
||||
|
||||
@ -54,7 +54,7 @@ const mockUser = {
|
||||
],
|
||||
},
|
||||
address: '',
|
||||
} satisfies User
|
||||
} satisfies AuthUser
|
||||
|
||||
test('handles mock data', async () => {
|
||||
mockQuery(getTasks, mockTasks)
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { WebSocketDefinition } from "wasp/server/webSocket";
|
||||
import { getFirstProviderUserId } from "wasp/auth/user";
|
||||
import { WebSocketDefinition } from 'wasp/server/webSocket'
|
||||
import { getFirstProviderUserId } from 'wasp/auth'
|
||||
|
||||
export const webSocketFn: WebSocketDefinition<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents
|
||||
> = (io, context) => {
|
||||
io.on("connection", (socket) => {
|
||||
const username = getFirstProviderUserId(socket.data.user) ?? "Unknown";
|
||||
console.log("a user connected: ", username);
|
||||
io.on('connection', (socket) => {
|
||||
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
|
||||
console.log('a user connected: ', username)
|
||||
|
||||
socket.on("chatMessage", async (msg) => {
|
||||
console.log("message: ", msg);
|
||||
io.emit("chatMessage", { id: "random", username, text: msg });
|
||||
});
|
||||
});
|
||||
};
|
||||
socket.on('chatMessage', async (msg) => {
|
||||
console.log('message: ', msg)
|
||||
io.emit('chatMessage', { id: 'random', username, text: msg })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
interface ServerToClientEvents {
|
||||
chatMessage: (msg: { id: string; username: string; text: string }) => void;
|
||||
chatMessage: (msg: { id: string; username: string; text: string }) => void
|
||||
}
|
||||
interface ClientToServerEvents {
|
||||
chatMessage: (msg: string) => void;
|
||||
chatMessage: (msg: string) => void
|
||||
}
|
||||
interface InterServerEvents {}
|
||||
|
Loading…
Reference in New Issue
Block a user