Update AuthUser API (#1915)

This commit is contained in:
Mihovil Ilakovac 2024-04-22 17:39:50 +02:00 committed by GitHub
parent c3e4a10d1b
commit 0e70547f7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 1940 additions and 762 deletions

View File

@ -1,5 +1,47 @@
# Changelog
## 0.14.0 (2024-04-22)
### 🎉 New Features
- Simplified Auth User API: Introduced a simpler API for accessing user auth fields (for example `username`, `email`, `isEmailVerified`) directly on the `user` object, eliminating the need for helper functions.
### ⚠️ Breaking Changes & Migration Guide
We had to make a couple of breaking changes to reach the new simpler API:
1. You don't need to use `getUsername` to access the username:
- Before: Used `getUsername` to access the username.
- After: Directly use `user.identities.username?.id`.
2. You don't need to use `getEmail` to access the email:
- Before: Used `getEmail` to access the email.
- After: Directly use `user.identities.email?.id`.
3. Better API for accessing `providerData`:
- Before: Required complex logic to access typed provider data.
- After: Directly use `user.identities.<provider>.<value>` for typed access.
4. Better API for accessing `getFirstProviderUserId`:
- Before: Used `getFirstProviderUserId(user)` to get the ID.
- After: Use `user.getFirstProviderUserId()` directly on the user object.
5. You don't need to use `findUserIdentity` any more:
- Before: Relied on `findUserIdentity` to check which user identity exists.
- After: Directly check `user.identities.<provider>` existence.
These changes improve code readability and lower the complexity of accessing user's auth fields. Follow the [detailed migration steps to update your project to 0.14.0](https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14).
### Note on Auth Helper Functions (`getUsername`, `getEmail` etc.)
These changes only apply to getting auth fields from the `user` object you receive from Wasp, for example in the `authRequired` enabled pages or `context.user` on the server. If you are fetching the user and auth fields with your own queries, you _can_ keep using most of the helpers. Read more [about using the auth helpers](https://wasp-lang.dev/docs/auth/entities#including-the-user-with-other-entities).
## 0.13.2 (2024-04-11)
### 🐞 Bug fixes

View File

@ -27,12 +27,12 @@ import { SocialButton } from '../social/SocialButton'
{=# isAnyPasswordBasedAuthEnabled =}
import { useHistory } from 'react-router-dom'
{=/ isAnyPasswordBasedAuthEnabled =}
{=# isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
import { useUsernameAndPassword } from '../usernameAndPassword/useUsernameAndPassword'
{=/ isUsernameAndPasswordAuthEnabled =}
{=# isEmailAuthEnabled =}
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isEmailAuthEnabled =}
import { useEmail } from '../email/useEmail'
{=/ isEmailAuthEnabled =}
{=/ enabledProviders.isEmailAuthEnabled =}
{=# areBothSocialAndPasswordBasedAuthEnabled =}
const OrContinueWith = styled('div', {
@ -105,15 +105,15 @@ const SocialAuthButtons = styled('div', {
}
})
{=/ isSocialAuthEnabled =}
{=# isGoogleAuthEnabled =}
{=# enabledProviders.isGoogleAuthEnabled =}
const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
{=/ isGoogleAuthEnabled =}
{=# isKeycloakAuthEnabled =}
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
const keycloakSignInUrl = `${config.apiUrl}{= keycloakSignInPath =}`
{=/ isKeycloakAuthEnabled =}
{=# isGitHubAuthEnabled =}
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
{=/ isGitHubAuthEnabled =}
{=/ enabledProviders.isGitHubAuthEnabled =}
{=!
// Since we allow users to add additional fields to the signup form, we don't
@ -151,7 +151,7 @@ export const LoginSignupForm = ({
{=/ isAnyPasswordBasedAuthEnabled =}
const hookForm = useForm<LoginSignupFormFields>()
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
{=# isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
const { handleSubmit } = useUsernameAndPassword({
isLogin,
onError: onErrorHandler,
@ -159,8 +159,8 @@ export const LoginSignupForm = ({
history.push('{= onAuthSucceededRedirectTo =}')
},
});
{=/ isUsernameAndPasswordAuthEnabled =}
{=# isEmailAuthEnabled =}
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isEmailAuthEnabled =}
const { handleSubmit } = useEmail({
isLogin,
onError: onErrorHandler,
@ -172,7 +172,7 @@ export const LoginSignupForm = ({
history.push('{= onAuthSucceededRedirectTo =}')
},
});
{=/ isEmailAuthEnabled =}
{=/ enabledProviders.isEmailAuthEnabled =}
{=# isAnyPasswordBasedAuthEnabled =}
async function onSubmit (data) {
setIsLoading(true);
@ -191,17 +191,17 @@ export const LoginSignupForm = ({
<SocialAuth>
<SocialAuthLabel>{cta} with</SocialAuthLabel>
<SocialAuthButtons gap='large' direction={socialButtonsDirection}>
{=# isGoogleAuthEnabled =}
{=# enabledProviders.isGoogleAuthEnabled =}
<SocialButton href={googleSignInUrl}><SocialIcons.Google/></SocialButton>
{=/ isGoogleAuthEnabled =}
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# isKeycloakAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
<SocialButton href={keycloakSignInUrl}><SocialIcons.Keycloak/></SocialButton>
{=/ isKeycloakAuthEnabled =}
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# isGitHubAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
<SocialButton href={gitHubSignInUrl}><SocialIcons.GitHub/></SocialButton>
{=/ isGitHubAuthEnabled =}
{=/ enabledProviders.isGitHubAuthEnabled =}
</SocialAuthButtons>
</SocialAuth>
{=/ isSocialAuthEnabled =}
@ -217,7 +217,7 @@ export const LoginSignupForm = ({
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
{=# isAnyPasswordBasedAuthEnabled =}
<Form onSubmit={hookFormHandleSubmit(onSubmit)}>
{=# isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
<FormItemGroup>
<FormLabel>Username</FormLabel>
<FormInput
@ -229,8 +229,8 @@ export const LoginSignupForm = ({
/>
{errors.username && <FormError>{errors.username.message}</FormError>}
</FormItemGroup>
{=/ isUsernameAndPasswordAuthEnabled =}
{=# isEmailAuthEnabled =}
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isEmailAuthEnabled =}
<FormItemGroup>
<FormLabel>E-mail</FormLabel>
<FormInput
@ -242,7 +242,7 @@ export const LoginSignupForm = ({
/>
{errors.email && <FormError>{errors.email.message}</FormError>}
</FormItemGroup>
{=/ isEmailAuthEnabled =}
{=/ enabledProviders.isEmailAuthEnabled =}
<FormItemGroup>
<FormLabel>Password</FormLabel>
<FormInput

View File

@ -1,4 +1,11 @@
// PUBLIC
export type { AuthUser } from '../server/_types'
// PUBLIC API
export {
getEmail,
getUsername,
getFirstProviderUserId,
} from './user.js'
export { getEmail, getUsername, getFirstProviderUserId, findUserIdentity } from './user.js'
// PUBLIC API
export {
type AuthUser,
} from '../server/auth/user.js';

View File

@ -6,12 +6,10 @@ import { type AuthUser } from 'wasp/auth'
import { auth } from "./lucia.js";
import type { Session } from "lucia";
import {
throwInvalidCredentialsError,
deserializeAndSanitizeProviderData,
} from "./utils.js";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
@ -81,29 +79,7 @@ async function getUser(userId: {= userEntityUpper =}['id']): Promise<AuthUser> {
throwInvalidCredentialsError()
}
// TODO: This logic must match the type in _types/index.ts (if we remove the
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
const deserializedIdentities = user.{= authFieldOnUserEntityName =}.{= identitiesFieldOnAuthEntityName =}.map((identity) => {
const deserializedProviderData = deserializeAndSanitizeProviderData(
identity.providerData,
{
shouldRemovePasswordField: true,
}
)
return {
...identity,
providerData: deserializedProviderData,
}
})
return {
...user,
auth: {
...user.auth,
identities: deserializedIdentities,
},
}
return createAuthUser(user);
}
// PRIVATE API

View File

@ -1,2 +1,2 @@
// todo(filip): turn into a proper import/path
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
export type { ProviderName } from 'wasp/server/_types'

View File

@ -3,7 +3,7 @@ import { deserialize as superjsonDeserialize } from 'superjson'
import { useQuery, addMetadataToQuery } from 'wasp/client/operations'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/client'
import type { AuthUser } from './types'
import type { AuthUser } from '../server/auth/user.js'
import { UseQueryResult } from '@tanstack/react-query'
// PUBLIC API

View File

@ -1,17 +1,24 @@
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types'
{{={= =}=}}
import { type {= authIdentityEntityName =} } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.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.
*/
import { type UserEntityWithAuth } from '../server/auth/user.js'
// PUBLIC API
export function getEmail(user: AuthUser): string | null {
export function getEmail(user: UserEntityWithAuth): string | null {
return findUserIdentity(user, "email")?.providerUserId ?? null;
}
// PUBLIC API
export function getUsername(user: AuthUser): string | null {
export function getUsername(user: UserEntityWithAuth): string | null {
return findUserIdentity(user, "username")?.providerUserId ?? null;
}
// PUBLIC API
export function getFirstProviderUserId(user?: AuthUser): string | null {
export function getFirstProviderUserId(user?: UserEntityWithAuth): string | null {
if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
return null;
}
@ -19,9 +26,11 @@ export function getFirstProviderUserId(user?: AuthUser): string | null {
return user.auth.identities[0].providerUserId ?? null;
}
// PUBLIC API
export function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined {
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): {= authIdentityEntityName =} | null {
if (!user.auth) {
return null;
}
return user.auth.identities.find(
(identity) => identity.providerName === providerName
);
) ?? null;
}

View File

@ -5,8 +5,6 @@ import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } fr
import { prisma } from 'wasp/server'
{=# isAuthEnabled =}
import {
type {= userEntityName =},
type {= authEntityName =},
type {= authIdentityEntityName =},
} from "wasp/entities"
import {
@ -14,6 +12,7 @@ import {
type UsernameProviderData,
type OAuthProviderData,
} from 'wasp/auth/utils'
import { type AuthUser } from 'wasp/auth'
{=/ isAuthEnabled =}
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
@ -88,20 +87,5 @@ type Context<Entities extends _Entity[]> = Expand<{
{=# isAuthEnabled =}
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { user?: AuthUser }>
// TODO: This type must match the logic in auth/session.js (if we remove the
// password field from the object there, we must do the same here). Ideally,
// these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
export type DeserializedAuthIdentity = Expand<Omit<{= authIdentityEntityName =}, 'providerData'> & {
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
}>
export type AuthUser = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: DeserializedAuthIdentity[]
} | null
}
export type { ProviderName } from 'wasp/auth/utils'
{=/ isAuthEnabled =}

View File

@ -0,0 +1,117 @@
{{={= =}=}}
import {
type {= userEntityName =},
type {= authEntityName =},
type {= authIdentityEntityName =},
} from '../../entities/index.js'
import {
type PossibleProviderData,
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
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* ```
* to get the benefits of the createAuthUser and the AuthUser 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.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, '{= authFieldOnUserEntityName =}'> & {
identities: {
{=# enabledProviders.isEmailAuthEnabled =}
email: Expand<UserFacingProviderData<'email'>> | null
{=/ enabledProviders.isEmailAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
username: Expand<UserFacingProviderData<'username'>> | null
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isGoogleAuthEnabled =}
google: Expand<UserFacingProviderData<'google'>> | null
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
keycloak: Expand<UserFacingProviderData<'keycloak'>> | null
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
github: Expand<UserFacingProviderData<'github'>> | null
{=/ enabledProviders.isGitHubAuthEnabled =}
},
getFirstProviderUserId: () => string | null,
}
type UserFacingProviderData<PN extends ProviderName> = {
id: string
} & Omit<PossibleProviderData[PN], 'hashedPassword'>
// PRIVATE API
export type UserEntityWithAuth = {= userEntityName =} & {
{= authFieldOnUserEntityName =}: AuthEntityWithIdentities | null
}
// PRIVATE API
export type AuthEntityWithIdentities = {= authEntityName =} & {
{= identitiesFieldOnAuthEntityName =}: {= authIdentityEntityName =}[]
}
// PRIVATE API
export function createAuthUser(user: UserEntityWithAuth): AuthUser {
const { {= authFieldOnUserEntityName =}, ...rest } = user
if (!{= authFieldOnUserEntityName =}) {
throw new Error(`🐝 Error: trying to create a user without auth data.
This should never happen, but it did which means there is a bug in the code.`)
}
const identities = {
{=# enabledProviders.isEmailAuthEnabled =}
email: getProviderInfo<'email'>({= authFieldOnUserEntityName =}, 'email'),
{=/ enabledProviders.isEmailAuthEnabled =}
{=# enabledProviders.isUsernameAndPasswordAuthEnabled =}
username: getProviderInfo<'username'>({= authFieldOnUserEntityName =}, 'username'),
{=/ enabledProviders.isUsernameAndPasswordAuthEnabled =}
{=# enabledProviders.isGoogleAuthEnabled =}
google: getProviderInfo<'google'>({= authFieldOnUserEntityName =}, 'google'),
{=/ enabledProviders.isGoogleAuthEnabled =}
{=# enabledProviders.isKeycloakAuthEnabled =}
keycloak: getProviderInfo<'keycloak'>({= authFieldOnUserEntityName =}, 'keycloak'),
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# enabledProviders.isGitHubAuthEnabled =}
github: getProviderInfo<'github'>({= authFieldOnUserEntityName =}, 'github'),
{=/ enabledProviders.isGitHubAuthEnabled =}
}
return {
...rest,
identities,
getFirstProviderUserId: () => getFirstProviderUserId(user),
}
}
function getProviderInfo<PN extends ProviderName>(
auth: AuthEntityWithIdentities,
providerName: PN
):
| UserFacingProviderData<PN>
| null {
const identity = getIdentity(auth, providerName)
if (!identity) {
return null
}
return {
...deserializeAndSanitizeProviderData<PN>(identity.providerData, {
shouldRemovePasswordField: true,
}),
id: identity.providerUserId,
}
}
function getIdentity(
auth: AuthEntityWithIdentities,
providerName: ProviderName
): {= authIdentityEntityName =} | null {
return auth.{= identitiesFieldOnAuthEntityName =}.find((i) => i.providerName === providerName) ?? null
}

View File

@ -264,6 +264,9 @@ waspComplexTest/.wasp/out/sdk/wasp/dist/server/api/index.js.map
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/index.d.ts
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/index.js
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/index.js.map
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/user.d.ts
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/user.js
waspComplexTest/.wasp/out/sdk/wasp/dist/server/auth/user.js.map
waspComplexTest/.wasp/out/sdk/wasp/dist/server/config.d.ts
waspComplexTest/.wasp/out/sdk/wasp/dist/server/config.js
waspComplexTest/.wasp/out/sdk/wasp/dist/server/config.js.map
@ -375,6 +378,7 @@ waspComplexTest/.wasp/out/sdk/wasp/server/_types/serialization.ts
waspComplexTest/.wasp/out/sdk/wasp/server/_types/taggedEntities.ts
waspComplexTest/.wasp/out/sdk/wasp/server/api/index.ts
waspComplexTest/.wasp/out/sdk/wasp/server/auth/index.ts
waspComplexTest/.wasp/out/sdk/wasp/server/auth/user.ts
waspComplexTest/.wasp/out/sdk/wasp/server/config.ts
waspComplexTest/.wasp/out/sdk/wasp/server/crud/index.ts
waspComplexTest/.wasp/out/sdk/wasp/server/crud/tasks.ts

View File

@ -95,7 +95,7 @@
"file",
"../out/sdk/wasp/auth/index.ts"
],
"f79d9aa0c8c5c0365c30c4db4e9c35946e1650266eeea1cf06fcce191211c911"
"a846aace0af3913411b6c77039cb04d37311d40e5e1f978fd960b38409ec9b66"
],
[
[
@ -137,28 +137,28 @@
"file",
"../out/sdk/wasp/auth/session.ts"
],
"b682047557c966d8f45e4ec1e27a0b4773cd5765af7864990edac6c0e3997db1"
"03f78291fb369497a07de2f5c2181857e80f158f5eb590b6c095e98c95ff9d14"
],
[
[
"file",
"../out/sdk/wasp/auth/types.ts"
],
"81afde3e152e92d6888f5cf67ab60711ab9cd156e09408d2a618dd02a71371fe"
"642e51e5d44dcc8a2730af4da67bb8012b39c4df39a724e7ee8540d138f610be"
],
[
[
"file",
"../out/sdk/wasp/auth/useAuth.ts"
],
"48a0e49de404ae16e84eeb3194295c1114c30c105ca227d0a4c428faf2b3a699"
"2fc317b5939816da10ae7709768c4dae52f541aa274620c1896c9d423b0c3122"
],
[
[
"file",
"../out/sdk/wasp/auth/user.ts"
],
"07164ea63b1aa89fa6279994f42f19b6f3552f897e9d9214c8799cadd9089be3"
"2b1911715a41f242cf3f4b8e3954db7146736300fa6888ec8090204eef6b0b6e"
],
[
[
@ -480,7 +480,7 @@
"file",
"../out/sdk/wasp/server/_types/index.ts"
],
"6993d64660f25759a15ccde4cd1f3a18e651044d362a348912203c0ba2284588"
"7a9bd946ac8b55a110a6d1ed93040c1ec4def2104b0de58961b56c678d31ab91"
],
[
[
@ -510,6 +510,13 @@
],
"61745079707aa8a4134e140b5cf33d5c9977d997da924de606cf606871438455"
],
[
[
"file",
"../out/sdk/wasp/server/auth/user.ts"
],
"76a7add6b2c64e66b583f69320e5cd8ee2595cef709b86d9264cb8d63f7b8adf"
],
[
[
"file",

View File

@ -1,4 +1,11 @@
// PUBLIC
export type { AuthUser } from '../server/_types'
// PUBLIC API
export {
getEmail,
getUsername,
getFirstProviderUserId,
} from './user.js'
export { getEmail, getUsername, getFirstProviderUserId, findUserIdentity } from './user.js'
// PUBLIC API
export {
type AuthUser,
} from '../server/auth/user.js';

View File

@ -5,12 +5,10 @@ import { type AuthUser } from 'wasp/auth'
import { auth } from "./lucia.js";
import type { Session } from "lucia";
import {
throwInvalidCredentialsError,
deserializeAndSanitizeProviderData,
} from "./utils.js";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
@ -80,29 +78,7 @@ async function getUser(userId: User['id']): Promise<AuthUser> {
throwInvalidCredentialsError()
}
// TODO: This logic must match the type in _types/index.ts (if we remove the
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
const deserializedIdentities = user.auth.identities.map((identity) => {
const deserializedProviderData = deserializeAndSanitizeProviderData(
identity.providerData,
{
shouldRemovePasswordField: true,
}
)
return {
...identity,
providerData: deserializedProviderData,
}
})
return {
...user,
auth: {
...user.auth,
identities: deserializedIdentities,
},
}
return createAuthUser(user);
}
// PRIVATE API

View File

@ -1,2 +1,2 @@
// todo(filip): turn into a proper import/path
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
export type { ProviderName } from 'wasp/server/_types'

View File

@ -2,7 +2,7 @@ import { deserialize as superjsonDeserialize } from 'superjson'
import { useQuery, addMetadataToQuery } from 'wasp/client/operations'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/client'
import type { AuthUser } from './types'
import type { AuthUser } from '../server/auth/user.js'
import { UseQueryResult } from '@tanstack/react-query'
// PUBLIC API

View File

@ -1,17 +1,23 @@
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types'
import { type AuthIdentity } from '../entities/index.js'
import { type ProviderName } from '../server/_types/index.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.
*/
import { type UserEntityWithAuth } from '../server/auth/user.js'
// PUBLIC API
export function getEmail(user: AuthUser): string | null {
export function getEmail(user: UserEntityWithAuth): string | null {
return findUserIdentity(user, "email")?.providerUserId ?? null;
}
// PUBLIC API
export function getUsername(user: AuthUser): string | null {
export function getUsername(user: UserEntityWithAuth): string | null {
return findUserIdentity(user, "username")?.providerUserId ?? null;
}
// PUBLIC API
export function getFirstProviderUserId(user?: AuthUser): string | null {
export function getFirstProviderUserId(user?: UserEntityWithAuth): string | null {
if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
return null;
}
@ -19,9 +25,11 @@ export function getFirstProviderUserId(user?: AuthUser): string | null {
return user.auth.identities[0].providerUserId ?? null;
}
// PUBLIC API
export function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined {
function findUserIdentity(user: UserEntityWithAuth, providerName: ProviderName): AuthIdentity | null {
if (!user.auth) {
return null;
}
return user.auth.identities.find(
(identity) => identity.providerName === providerName
);
) ?? null;
}

View File

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

View File

@ -1,2 +1,3 @@
export { getEmail, getUsername, getFirstProviderUserId, findUserIdentity } from './user.js';
// PUBLIC API
export { getEmail, getUsername, getFirstProviderUserId, } from './user.js';
//# sourceMappingURL=index.js.map

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../auth/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,OAAO,EACL,QAAQ,EACR,WAAW,EACX,sBAAsB,GACvB,MAAM,WAAW,CAAA"}

View File

@ -1,6 +1,7 @@
import { auth } from "./lucia.js";
import { throwInvalidCredentialsError, deserializeAndSanitizeProviderData, } from "./utils.js";
import { throwInvalidCredentialsError } from "./utils.js";
import { prisma } from 'wasp/server';
import { createAuthUser } from "../server/auth/user.js";
// PRIVATE API
// Creates a new session for the `authId` in the database
export async function createSession(authId) {
@ -53,17 +54,7 @@ async function getUser(userId) {
if (!user) {
throwInvalidCredentialsError();
}
// TODO: This logic must match the type in _types/index.ts (if we remove the
// password field from the object here, we must to do the same there).
// Ideally, these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
const deserializedIdentities = user.auth.identities.map((identity) => {
const deserializedProviderData = deserializeAndSanitizeProviderData(identity.providerData, {
shouldRemovePasswordField: true,
});
return Object.assign(Object.assign({}, identity), { providerData: deserializedProviderData });
});
return Object.assign(Object.assign({}, user), { auth: Object.assign(Object.assign({}, user.auth), { identities: deserializedIdentities }) });
return createAuthUser(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,EACL,4BAA4B,EAC5B,kCAAkC,GACnC,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,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,4EAA4E;IAC5E,sEAAsE;IACtE,0DAA0D;IAC1D,+CAA+C;IAC/C,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QACnE,MAAM,wBAAwB,GAAG,kCAAkC,CACjE,QAAQ,CAAC,YAAY,EACrB;YACE,yBAAyB,EAAE,IAAI;SAChC,CACF,CAAA;QACD,uCACK,QAAQ,KACX,YAAY,EAAE,wBAAwB,IACvC;IACH,CAAC,CAAC,CAAA;IACF,uCACK,IAAI,KACP,IAAI,kCACC,IAAI,CAAC,IAAI,KACZ,UAAU,EAAE,sBAAsB,OAErC;AACH,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,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"}

View File

@ -1 +1 @@
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types';
export type { ProviderName } from 'wasp/server/_types';

View File

@ -1,4 +1,4 @@
import type { AuthUser } from './types';
import type { AuthUser } from '../server/auth/user.js';
import { UseQueryResult } from '@tanstack/react-query';
export declare const getMe: () => Promise<AuthUser | null>;
export default function useAuth(queryFnArgs?: unknown, config?: any): UseQueryResult<AuthUser>;

View File

@ -1,5 +1,8 @@
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types';
export declare function getEmail(user: AuthUser): string | null;
export declare function getUsername(user: AuthUser): string | null;
export declare function getFirstProviderUserId(user?: AuthUser): string | null;
export declare function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined;
/**
* We split the user.ts code into two files to avoid some server-only
* code (Oslo's hashing functions) being imported on the client.
*/
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;

View File

@ -16,8 +16,11 @@ export function getFirstProviderUserId(user) {
}
return (_a = user.auth.identities[0].providerUserId) !== null && _a !== void 0 ? _a : null;
}
// PUBLIC API
export function findUserIdentity(user, providerName) {
return user.auth.identities.find((identity) => identity.providerName === providerName);
function findUserIdentity(user, providerName) {
var _a;
if (!user.auth) {
return null;
}
return (_a = user.auth.identities.find((identity) => identity.providerName === providerName)) !== null && _a !== void 0 ? _a : null;
}
//# sourceMappingURL=user.js.map

View File

@ -1 +1 @@
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../auth/user.ts"],"names":[],"mappings":"AAEA,aAAa;AACb,MAAM,UAAU,QAAQ,CAAC,IAAc;;IACrC,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACjE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,WAAW,CAAC,IAAc;;IACxC,OAAO,MAAA,MAAA,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,0CAAE,cAAc,mCAAI,IAAI,CAAC;AACpE,CAAC;AAED,aAAa;AACb,MAAM,UAAU,sBAAsB,CAAC,IAAe;;IACpD,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,aAAa;AACb,MAAM,UAAU,gBAAgB,CAAC,IAAc,EAAE,YAA0B;IACzE,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAC9B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,KAAK,YAAY,CACrD,CAAC;AACJ,CAAC"}
{"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"}

View File

@ -2,8 +2,7 @@ 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 AuthUser } from 'wasp/auth';
import { type _Entity } from "./taggedEntities";
import { type Payload } from "./serialization";
export * from "./taggedEntities";
@ -30,12 +29,4 @@ type Context<Entities extends _Entity[]> = Expand<{
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & {
user?: AuthUser;
}>;
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';

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/_types/index.ts"],"names":[],"mappings":"AAiBA,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/_types/index.ts"],"names":[],"mappings":"AAgBA,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA"}

View File

@ -0,0 +1,33 @@
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';
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* ```
* to get the benefits of the createAuthUser and the AuthUser 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.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null;
};
getFirstProviderUserId: () => string | null;
};
type UserFacingProviderData<PN extends ProviderName> = {
id: string;
} & Omit<PossibleProviderData[PN], 'hashedPassword'>;
export type UserEntityWithAuth = User & {
auth: AuthEntityWithIdentities | null;
};
export type AuthEntityWithIdentities = Auth & {
identities: AuthIdentity[];
};
export declare function createAuthUser(user: UserEntityWithAuth): AuthUser;
export {};

View File

@ -0,0 +1,39 @@
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { deserializeAndSanitizeProviderData } from '../../auth/utils.js';
import { getFirstProviderUserId } from '../../auth/user.js';
// PRIVATE API
export function createAuthUser(user) {
const { auth } = user, rest = __rest(user, ["auth"]);
if (!auth) {
throw new Error(`🐝 Error: trying to create a user without auth data.
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) });
}
function getProviderInfo(auth, providerName) {
const identity = getIdentity(auth, providerName);
if (!identity) {
return null;
}
return Object.assign(Object.assign({}, deserializeAndSanitizeProviderData(identity.providerData, {
shouldRemovePasswordField: true,
})), { id: identity.providerUserId });
}
function getIdentity(auth, providerName) {
var _a;
return (_a = auth.identities.find((i) => i.providerName === providerName)) !== null && _a !== void 0 ? _a : null;
}
//# sourceMappingURL=user.js.map

View File

@ -0,0 +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"}

View File

@ -3,8 +3,6 @@ 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 {
@ -12,6 +10,7 @@ import {
type UsernameProviderData,
type OAuthProviderData,
} from 'wasp/auth/utils'
import { type AuthUser } from 'wasp/auth'
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
@ -82,19 +81,4 @@ type Context<Entities extends _Entity[]> = Expand<{
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'

View File

@ -0,0 +1,88 @@
import {
type User,
type Auth,
type AuthIdentity,
} from '../../entities/index.js'
import {
type PossibleProviderData,
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
/**
* Ideally, we'd do something like this:
* ```
* export type AuthUser = ReturnType<typeof createAuthUser>
* ```
* to get the benefits of the createAuthUser and the AuthUser 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.
*
* TODO: Change this once/if we switch to strict mode. https://github.com/wasp-lang/wasp/issues/1938
*/
export type AuthUser = Omit<UserEntityWithAuth, 'auth'> & {
identities: {
google: Expand<UserFacingProviderData<'google'>> | null
},
getFirstProviderUserId: () => string | null,
}
type UserFacingProviderData<PN extends ProviderName> = {
id: string
} & Omit<PossibleProviderData[PN], 'hashedPassword'>
// PRIVATE API
export type UserEntityWithAuth = User & {
auth: AuthEntityWithIdentities | null
}
// PRIVATE API
export type AuthEntityWithIdentities = Auth & {
identities: AuthIdentity[]
}
// PRIVATE API
export function createAuthUser(user: UserEntityWithAuth): AuthUser {
const { auth, ...rest } = user
if (!auth) {
throw new Error(`🐝 Error: trying to create a user without auth data.
This should never happen, but it did which means there is a bug in the code.`)
}
const identities = {
google: getProviderInfo<'google'>(auth, 'google'),
}
return {
...rest,
identities,
getFirstProviderUserId: () => getFirstProviderUserId(user),
}
}
function getProviderInfo<PN extends ProviderName>(
auth: AuthEntityWithIdentities,
providerName: PN
):
| UserFacingProviderData<PN>
| null {
const identity = getIdentity(auth, providerName)
if (!identity) {
return null
}
return {
...deserializeAndSanitizeProviderData<PN>(identity.providerData, {
shouldRemovePasswordField: true,
}),
id: identity.providerUserId,
}
}
function getIdentity(
auth: AuthEntityWithIdentities,
providerName: ProviderName
): AuthIdentity | null {
return auth.identities.find((i) => i.providerName === providerName) ?? null
}

View File

@ -1,6 +1,6 @@
import { Link, routes } from "wasp/client/router";
import { logout } from "wasp/client/auth";
import { getUsername } from "wasp/auth";
import { Link, routes } from 'wasp/client/router'
import { logout } from 'wasp/client/auth'
import { getUsername } from 'wasp/auth'
import './Main.css'

View File

@ -27,7 +27,12 @@ export const getAllTasks = (async (args, context) => {
include: {
auth: {
include: {
identities: true,
identities: {
select: {
providerName: true,
providerUserId: true,
}
},
},
},
},

View File

@ -22,7 +22,7 @@ app todoApp {
configFn: import { config } from "@src/auth/github.js",
userSignupFields: import { userSignupFields } from "@src/auth/github.js"
},
keycloak: {},
// keycloak: {},
email: {
userSignupFields: import { userSignupFields } from "@src/auth/email.js",
fromField: {

View File

@ -1,4 +1,4 @@
import { type AuthUser as User } from 'wasp/auth'
import { type AuthUser } from 'wasp/auth'
import { mockServer, renderInContext } from 'wasp/client/test'
import { getTasks, getDate } from 'wasp/client/operations'
import { test, expect } from 'vitest'
@ -36,21 +36,12 @@ test('handles mock data', async () => {
})
const mockUser = {
id: 12,
auth: {
id: '123',
userId: 12,
identities: [
{
authId: '123',
providerName: 'email',
providerUserId: 'elon@tesla.com',
providerData: '',
},
],
identities: {
email: {
id: 'elon@tesla.com',
},
},
address: null,
} satisfies User
} as AuthUser
test('handles multiple mock data sources', async () => {
mockQuery(getMe, mockUser)

View File

@ -1,10 +1,9 @@
import { getFirstProviderUserId } from "wasp/auth";
import { type MiddlewareConfigFn } from "wasp/server";
import { type BarBaz, type FooBar, type WebhookCallback } from "wasp/server/api";
import { type MiddlewareConfigFn } from 'wasp/server'
import { type BarBaz, type FooBar, type WebhookCallback } from 'wasp/server/api'
import express from 'express'
export const fooBar: FooBar = (_req, res, context) => {
const username = getFirstProviderUserId(context?.user) ?? 'Anonymous'
const username = context.user?.getFirstProviderUserId()
res.json({ msg: `Hello, ${username}!` })
}

View File

@ -1,7 +1,8 @@
import { PrismaClient } from '@prisma/client/index.js'
import { type DbSeedFn } from "wasp/server";
import { type DbSeedFn } from 'wasp/server'
import { sanitizeAndSerializeProviderData } from 'wasp/server/auth'
import { createTask } from './actions.js'
import { type AuthUser } from 'wasp/auth'
async function createUser(prismaClient: PrismaClient, data: any) {
const newUser = await prismaClient.user.create({
@ -34,14 +35,9 @@ async function createUser(prismaClient: PrismaClient, data: any) {
},
})
if (newUser.auth?.identities) {
newUser.auth.identities = newUser.auth.identities.map((identity) => {
identity.providerData = JSON.parse(identity.providerData)
return identity
})
}
return newUser
return {
id: newUser.id,
} as AuthUser
}
export const devSeedSimple: DbSeedFn = async (prismaClient) => {

View File

@ -1,16 +1,20 @@
import { type AuthUser as User } from "wasp/auth";
import { type ServerToClientPayload, useSocket, useSocketListener } from "wasp/client/webSocket";
import { Link, routes } from "wasp/client/router";
import { api } from "wasp/client/api";
import { type AuthUser } from 'wasp/auth'
import {
type ServerToClientPayload,
useSocket,
useSocketListener,
} from 'wasp/client/webSocket'
import { Link, routes } from 'wasp/client/router'
import { api } from 'wasp/client/api'
import React, { useEffect, useRef, useState } from 'react'
import { getName, getProviderData } from '../user'
import { getName } from '../user'
async function fetchCustomRoute() {
const res = await api.get('/foo/bar')
console.log(res.data)
}
export const ProfilePage = ({ user }: { user: User }) => {
export const ProfilePage = ({ user }: { user: AuthUser }) => {
const [messages, setMessages] = useState<
ServerToClientPayload<'chatMessage'>[]
>([])
@ -41,15 +45,13 @@ export const ProfilePage = ({ user }: { user: User }) => {
))
const connectionIcon = isConnected ? '🟢' : '🔴'
const providerData = getProviderData(user)
return (
<>
<h2>Profile page</h2>
<div>
Hello <strong>{getName(user)}</strong>! Your status is{' '}
<strong>
{providerData && providerData.isEmailVerified
{user.identities.email && user.identities.email.isEmailVerified
? 'verfied'
: 'unverified'}
</strong>

View File

@ -1,42 +1,27 @@
import { getEmail, findUserIdentity, type AuthUser as User } from 'wasp/auth'
import { type AuthUser } from 'wasp/auth'
export function getName(user?: User) {
export function getName(user?: AuthUser) {
if (!user) {
return null
}
// We use multiple auth methods, so we need to check which one is available.
const emailIdentity = findUserIdentity(user, 'email')
if (emailIdentity !== undefined) {
return getEmail(user)
if (user.identities.email !== null) {
return user.identities.email.id
}
const googleIdentity = findUserIdentity(user, 'google')
if (googleIdentity !== undefined) {
return `Google user ${googleIdentity.providerUserId}`
if (user.identities.google !== null) {
return `Google user ${user.identities.google.id}`
}
const githubIdentity = findUserIdentity(user, 'github')
if (githubIdentity !== undefined) {
return `GitHub user ${githubIdentity.providerUserId}`
if (user.identities.github !== null) {
return `GitHub user ${user.identities.github.id}`
}
const keycloakIdentity = findUserIdentity(user, 'keycloak')
if (keycloakIdentity) {
return `Keycloak user ${keycloakIdentity.providerUserId}`
}
// if (user.identities.keycloak !== undefined) {
// return `Keycloak user ${user.identities.keycloak.id}`
// }
// If we don't know how to get the name, return null.
return null
}
export function getProviderData(user?: User) {
if (!user) {
return null
}
const emailIdentity = findUserIdentity(user, 'email')
return emailIdentity && 'isEmailVerified' in emailIdentity.providerData
? emailIdentity.providerData
: null
}

View File

@ -1,5 +1,4 @@
import { getFirstProviderUserId } from "wasp/auth";
import { type WebSocketDefinition } from "wasp/server/webSocket";
import { type WebSocketDefinition } from 'wasp/server/webSocket'
import { v4 as uuidv4 } from 'uuid'
export const webSocketFn: WebSocketDefinition<
@ -8,7 +7,7 @@ export const webSocketFn: WebSocketDefinition<
InterServerEvents
> = (io, context) => {
io.on('connection', (socket) => {
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
const username = socket.data.user?.getFirstProviderUserId() ?? 'Unknown'
console.log('a user connected: ', username)
socket.on('chatMessage', async (msg) => {

View File

@ -24,32 +24,31 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@lucia-auth/adapter-prisma": "^4.0.0-beta.9",
"@lucia-auth/adapter-prisma": "^4.0.0",
"@prisma/client": "4.16.2",
"@stitches/react": "^1.2.8",
"@tanstack/react-query": "^4.29.0",
"@testing-library/jest-dom": "^6.3.0",
"@testing-library/react": "^14.1.2",
"@types/express-serve-static-core": "^4.17.13",
"@types/react-router-dom": "^5.3.3",
"@vitest/ui": "^1.2.1",
"autoprefixer": "^10.4.13",
"axios": "^1.4.0",
"express": "~4.18.1",
"jsdom": "^21.1.1",
"jsonwebtoken": "^8.5.1",
"lodash.merge": "^4.6.2",
"lucia": "^3.0.0-beta.14",
"lucia": "^3.0.1",
"mitt": "3.0.0",
"msw": "^1.1.0",
"nodemailer": "^6.9.1",
"oslo": "^1.1.2",
"pg-boss": "^8.4.2",
"postcss": "^8.4.21",
"prisma": "4.16.2",
"react": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-router-dom": "^5.3.3",
"secure-password": "^4.0.0",
"sodium-native": "3.3.0",
"superjson": "^1.12.2",
"tailwindcss": "^3.2.7",
"uuid": "^9.0.0",
@ -59,13 +58,491 @@
"@tsconfig/node18": "latest"
}
},
".wasp/out/sdk/wasp/node_modules/sodium-native": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.3.0.tgz",
"integrity": "sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==",
"hasInstallScript": true,
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz",
"integrity": "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@node-rs/argon2-android-arm-eabi": "1.7.0",
"@node-rs/argon2-android-arm64": "1.7.0",
"@node-rs/argon2-darwin-arm64": "1.7.0",
"@node-rs/argon2-darwin-x64": "1.7.0",
"@node-rs/argon2-freebsd-x64": "1.7.0",
"@node-rs/argon2-linux-arm-gnueabihf": "1.7.0",
"@node-rs/argon2-linux-arm64-gnu": "1.7.0",
"@node-rs/argon2-linux-arm64-musl": "1.7.0",
"@node-rs/argon2-linux-x64-gnu": "1.7.0",
"@node-rs/argon2-linux-x64-musl": "1.7.0",
"@node-rs/argon2-wasm32-wasi": "1.7.0",
"@node-rs/argon2-win32-arm64-msvc": "1.7.0",
"@node-rs/argon2-win32-ia32-msvc": "1.7.0",
"@node-rs/argon2-win32-x64-msvc": "1.7.0"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-android-arm-eabi": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz",
"integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-android-arm64": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz",
"integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-darwin-arm64": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz",
"integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-darwin-x64": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz",
"integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-freebsd-x64": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz",
"integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz",
"integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-linux-arm64-gnu": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz",
"integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-linux-arm64-musl": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz",
"integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-linux-x64-gnu": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz",
"integrity": "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-linux-x64-musl": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz",
"integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-wasm32-wasi": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz",
"integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
"@emnapi/core": "^0.45.0",
"@emnapi/runtime": "^0.45.0",
"@tybys/wasm-util": "^0.8.1",
"memfs-browser": "^3.4.13000"
},
"engines": {
"node": ">=14.0.0"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-win32-arm64-msvc": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz",
"integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-win32-ia32-msvc": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz",
"integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/argon2-win32-x64-msvc": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz",
"integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz",
"integrity": "sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig==",
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@node-rs/bcrypt-android-arm-eabi": "1.9.0",
"@node-rs/bcrypt-android-arm64": "1.9.0",
"@node-rs/bcrypt-darwin-arm64": "1.9.0",
"@node-rs/bcrypt-darwin-x64": "1.9.0",
"@node-rs/bcrypt-freebsd-x64": "1.9.0",
"@node-rs/bcrypt-linux-arm-gnueabihf": "1.9.0",
"@node-rs/bcrypt-linux-arm64-gnu": "1.9.0",
"@node-rs/bcrypt-linux-arm64-musl": "1.9.0",
"@node-rs/bcrypt-linux-x64-gnu": "1.9.0",
"@node-rs/bcrypt-linux-x64-musl": "1.9.0",
"@node-rs/bcrypt-wasm32-wasi": "1.9.0",
"@node-rs/bcrypt-win32-arm64-msvc": "1.9.0",
"@node-rs/bcrypt-win32-ia32-msvc": "1.9.0",
"@node-rs/bcrypt-win32-x64-msvc": "1.9.0"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-android-arm-eabi": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz",
"integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-android-arm64": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz",
"integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-darwin-arm64": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz",
"integrity": "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-darwin-x64": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz",
"integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-freebsd-x64": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz",
"integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz",
"integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-linux-arm64-gnu": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz",
"integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-linux-arm64-musl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz",
"integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-linux-x64-gnu": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz",
"integrity": "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-linux-x64-musl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz",
"integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-wasm32-wasi": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz",
"integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/core": "^0.45.0",
"@emnapi/runtime": "^0.45.0",
"@tybys/wasm-util": "^0.8.1",
"memfs-browser": "^3.4.13000"
},
"engines": {
"node": ">=14.0.0"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-win32-arm64-msvc": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz",
"integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-win32-ia32-msvc": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz",
"integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/@node-rs/bcrypt-win32-x64-msvc": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz",
"integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
".wasp/out/sdk/wasp/node_modules/oslo": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/oslo/-/oslo-1.2.0.tgz",
"integrity": "sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==",
"dependencies": {
"@node-rs/argon2": "1.7.0",
"@node-rs/bcrypt": "1.9.0"
}
},
"node_modules/@adobe/css-tools": {
@ -1775,6 +2252,11 @@
"@types/send": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -1837,6 +2319,25 @@
"@types/react": "*"
}
},
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -2421,11 +2922,6 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
@ -3047,14 +3543,6 @@
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3527,6 +4015,12 @@
"node": ">= 0.6"
}
},
"node_modules/fs-monkey": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
"integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==",
"optional": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -4444,51 +4938,6 @@
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="
},
"node_modules/jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=4",
"npm": ">=1.4.28"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -4532,46 +4981,16 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -4686,6 +5105,27 @@
"node": ">= 0.6"
}
},
"node_modules/memfs": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
"integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
"optional": true,
"dependencies": {
"fs-monkey": "^1.0.4"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/memfs-browser": {
"version": "3.5.10302",
"resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz",
"integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==",
"optional": true,
"dependencies": {
"memfs": "3.5.3"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@ -4931,11 +5371,6 @@
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoassert": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
"integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ=="
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@ -4999,16 +5434,6 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
"integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -6154,23 +6579,6 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/secure-password": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/secure-password/-/secure-password-4.0.0.tgz",
"integrity": "sha512-B268T/tx+hq7q85KH6gonEqK/lhrLhNtzYzqojuMtBPVFBtwiIwxqF+4yr9POsJu5cIxbJyM66eYfXZiPZUXRA==",
"dependencies": {
"nanoassert": "^1.0.0",
"sodium-native": "^3.1.1"
}
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
@ -6342,15 +6750,6 @@
"node": ">= 10"
}
},
"node_modules/sodium-native": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz",
"integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==",
"hasInstallScript": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@ -1,8 +1,8 @@
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, getProviderData } from '../user'
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')
@ -15,7 +15,6 @@ export const ProfilePage = ({ user }: { user: User }) => {
}, [])
const name = getName(user)
const providerData = getProviderData(user)
return (
<>
@ -23,7 +22,7 @@ export const ProfilePage = ({ user }: { user: User }) => {
<div>
Hello <strong>{name}</strong>! Your status is{' '}
<strong>
{providerData && providerData.isEmailVerified
{user.identities.email && user.identities.email.isEmailVerified
? 'verfied'
: 'unverified'}
</strong>

View File

@ -1,32 +1,19 @@
import { getEmail, findUserIdentity, type AuthUser as User } from "wasp/auth";
import { type AuthUser } from 'wasp/auth'
export function getName(user?: User) {
export function getName(user?: AuthUser) {
if (!user) {
return null
}
// We use multiple auth methods, so we need to check which one is available.
const emailIdentity = findUserIdentity(user, 'email')
if (emailIdentity) {
return getEmail(user)
if (user.identities.email !== null) {
return user.identities.email.id
}
const googleIdentity = findUserIdentity(user, 'google')
if (googleIdentity) {
return `Google user ${googleIdentity.providerUserId}`
if (user.identities.google !== null) {
return `Google user ${user.identities.google.id}`
}
// If we don't know how to get the name, return null.
return null
}
export function getProviderData(user?: User) {
if (!user) {
return null
}
const emailIdentity = findUserIdentity(user, 'email')
return emailIdentity && 'isEmailVerified' in emailIdentity.providerData
? emailIdentity.providerData
: null
}

View File

@ -1,10 +1,9 @@
import { getFirstProviderUserId } from "wasp/auth";
import { type MiddlewareConfigFn } from "wasp/server";
import { type BarBaz, type FooBar, type WebhookCallback } from "wasp/server/api";
import { type MiddlewareConfigFn } from 'wasp/server'
import { type BarBaz, type FooBar, type WebhookCallback } from 'wasp/server/api'
import express from 'express'
export const fooBar: FooBar = (_req, res, context) => {
const username = getFirstProviderUserId(context?.user) ?? 'Anonymous'
const username = context?.user?.getFirstProviderUserId() ?? 'Anonymous'
res.json({ msg: `Hello, ${username}!` })
}

View File

@ -247,7 +247,6 @@ generateLayoutComponent newProjectDetails =
[trimming|
import { Link } from "react-router-dom";
import { useAuth, logout } from "wasp/client/auth";
import { getUsername } from "wasp/auth";
import "./Main.css";
export const Layout = ({ children }) => {
@ -262,7 +261,7 @@ generateLayoutComponent newProjectDetails =
</Link>
{ user ? (
<span>
Hi, {getUsername(user)}!{' '}
Hi, {user.identities.username?.id}!{' '}
<button onClick={logout} className="text-xl2 underline">
(Log out)
</button>

View File

@ -1,6 +1,9 @@
module Wasp.Generator.AuthProviders where
import Data.Aeson (KeyValue ((.=)), object)
import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust)
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.Generator.AuthProviders.Common (makeProviderId)
import qualified Wasp.Generator.AuthProviders.Email as E
import qualified Wasp.Generator.AuthProviders.Local as L
@ -43,3 +46,13 @@ emailAuthProvider =
{ E._providerId = fromJust $ makeProviderId "email",
E._displayName = "Email and password"
}
getEnabledAuthProvidersJson :: AS.Auth.Auth -> Aeson.Value
getEnabledAuthProvidersJson auth =
object
[ "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth,
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
]

View File

@ -156,11 +156,7 @@ genEntitiesAndServerTypesDirs spec =
object
[ "entities" .= allEntities,
"isAuthEnabled" .= isJust maybeUserEntityName,
"userEntityName" .= userEntityName,
"authEntityName" .= DbAuth.authEntityName,
"authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName,
"authIdentityEntityName" .= DbAuth.authIdentityEntityName,
"identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName,
"userFieldName" .= toLowerFirst userEntityName
]
)

View File

@ -11,6 +11,7 @@ import Wasp.Generator.AuthProviders
googleAuthProvider,
keycloakAuthProvider,
)
import qualified Wasp.Generator.AuthProviders as AuthProviders
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
@ -115,19 +116,10 @@ genLoginSignupForm auth =
"areBothSocialAndPasswordBasedAuthEnabled" .= areBothSocialAndPasswordBasedAuthEnabled,
"isAnyPasswordBasedAuthEnabled" .= isAnyPasswordBasedAuthEnabled,
"isSocialAuthEnabled" .= AS.Auth.isExternalAuthEnabled auth,
-- Google
"isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"googleSignInPath" .= OAuth.serverLoginUrl googleAuthProvider,
-- Keycloak
"isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth,
"keycloakSignInPath" .= OAuth.serverLoginUrl keycloakAuthProvider,
-- GitHub
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"gitHubSignInPath" .= OAuth.serverLoginUrl gitHubAuthProvider,
-- Username and password
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
-- Email
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
"enabledProviders" .= AuthProviders.getEnabledAuthProvidersJson auth
]
areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled
isAnyPasswordBasedAuthEnabled = AS.Auth.isUsernameAndPasswordAuthEnabled auth || AS.Auth.isEmailAuthEnabled auth

View File

@ -29,7 +29,9 @@ genAuth spec =
Nothing -> return []
Just auth ->
-- shared stuff
sequence [genFileCopy [relfile|auth/user.ts|]]
sequence
[ genUserTs
]
-- client stuff
<++> sequence
[ genFileCopy [relfile|auth/helpers/user.ts|],
@ -131,3 +133,8 @@ genProvidersTypes auth = return $ C.mkTmplFdWithData [relfile|auth/providers/typ
userEntityName = AS.refName $ AS.Auth.userEntity auth
tmplData = object ["userEntityUpper" .= (userEntityName :: String)]
genUserTs :: Generator FileDraft
genUserTs = return $ C.mkTmplFdWithData [relfile|auth/user.ts|] tmplData
where
tmplData = object ["authIdentityEntityName" .= DbAuth.authIdentityEntityName]

View File

@ -3,13 +3,12 @@ module Wasp.Generator.SdkGenerator.Client.AuthG
)
where
import Data.Aeson (object, (.=))
import qualified Data.Aeson as Aeson
import StrongPath (File', Path', Rel, relfile)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
import qualified Wasp.Generator.AuthProviders as AuthProviders
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.SdkGenerator.Common (SdkTemplatesDir)
@ -40,7 +39,7 @@ genAuthIndex auth =
[relfile|client/auth/index.ts|]
tmplData
where
tmplData = getAuthProvidersJson auth
tmplData = AuthProviders.getEnabledAuthProvidersJson auth
genAuthUI :: AS.Auth.Auth -> Generator FileDraft
genAuthUI auth =
@ -49,7 +48,7 @@ genAuthUI auth =
[relfile|client/auth/ui.ts|]
tmplData
where
tmplData = getAuthProvidersJson auth
tmplData = AuthProviders.getEnabledAuthProvidersJson auth
genAuthEmail :: AS.Auth.Auth -> Generator [FileDraft]
genAuthEmail auth =
@ -81,15 +80,5 @@ genAuthGitHub auth =
then sequence [genFileCopy [relfile|client/auth/github.ts|]]
else return []
getAuthProvidersJson :: AS.Auth.Auth -> Aeson.Value
getAuthProvidersJson auth =
object
[ "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth,
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
]
genFileCopy :: Path' (Rel SdkTemplatesDir) File' -> Generator FileDraft
genFileCopy = return . C.mkTmplFd

View File

@ -4,12 +4,14 @@ module Wasp.Generator.SdkGenerator.Server.AuthG
where
import Data.Aeson (object, (.=))
import qualified Data.Aeson as Aeson
import StrongPath (File', Path', Rel, relfile)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
import qualified Wasp.Generator.AuthProviders as AuthProviders
import qualified Wasp.Generator.DbGenerator.Auth as DbAuth
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.SdkGenerator.Common (SdkTemplatesDir)
@ -22,7 +24,8 @@ genNewServerApi spec =
Nothing -> return []
Just auth ->
sequence
[ genAuthIndex auth
[ genAuthIndex auth,
genAuthUser auth
]
<++> genAuthEmail auth
<++> genAuthUsername auth
@ -36,7 +39,26 @@ genAuthIndex auth =
[relfile|server/auth/index.ts|]
tmplData
where
tmplData = getAuthProvidersJson auth
tmplData = AuthProviders.getEnabledAuthProvidersJson auth
genAuthUser :: AS.Auth.Auth -> Generator FileDraft
genAuthUser auth =
return $
C.mkTmplFdWithData
[relfile|server/auth/user.ts|]
tmplData
where
userEntityName = AS.refName $ AS.Auth.userEntity auth
tmplData =
object
[ "userEntityName" .= userEntityName,
"authEntityName" .= DbAuth.authEntityName,
"authFieldOnUserEntityName" .= DbAuth.authFieldOnUserEntityName,
"authIdentityEntityName" .= DbAuth.authIdentityEntityName,
"identitiesFieldOnAuthEntityName" .= DbAuth.identitiesFieldOnAuthEntityName,
"enabledProviders" .= AuthProviders.getEnabledAuthProvidersJson auth
]
genAuthEmail :: AS.Auth.Auth -> Generator [FileDraft]
genAuthEmail auth =
@ -50,15 +72,5 @@ genAuthUsername auth =
then sequence [genFileCopy [relfile|server/auth/username.ts|]]
else return []
getAuthProvidersJson :: AS.Auth.Auth -> Aeson.Value
getAuthProvidersJson auth =
object
[ "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth,
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
]
genFileCopy :: Path' (Rel SdkTemplatesDir) File' -> Generator FileDraft
genFileCopy = return . C.mkTmplFd

View File

@ -0,0 +1,7 @@
<small>
Read more about accessing the user data in the [Accessing User Data](/auth/entities/entities.md#accessing-the-auth-fields) section of the docs.
</small>
<!-- This snippet is used in all of the individual auth provider docs. -->

View File

@ -1 +1,5 @@
You can read more about how the `User` entity is connected to the rest of the auth system in the [Auth Entities](./entities) section of the docs.
<small>
You can read more about how the `User` is connected to the rest of the auth system and how you can access the user data in the [Accessing User Data](./entities) section of the docs.
</small>

View File

@ -5,9 +5,10 @@ title: Email
import { Required } from '@site/src/components/Tag';
import MultipleIdentitiesWarning from './\_multiple-identities-warning.md';
import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md';
import GetEmail from './entities/\_get-email.md';
import UserSignupFieldsExplainer from './\_user-signup-fields-explainer.md';
import UserFields from './\_user-fields.md';
import EmailData from './entities/\_email-data.md';
import AccessingUserDataNote from './\_accessing-user-data-note.md';
Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.
@ -814,13 +815,11 @@ We suggest using the built-in field validators for your authentication flow. You
To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [auth overview docs](../auth/overview).
### `getEmail`
When you receive the `user` object [on the client or the server](./overview.md#accessing-the-logged-in-user), you'll be able to access the user's email and other information like this:
If you are looking to access the user's email in your code, you can do that by accessing the info about the user that is stored in the `user.auth.identities` array.
<EmailData />
To make things a bit easier for you, Wasp offers the `getEmail` helper.
<GetEmail />
<AccessingUserDataNote />
## API Reference

View File

@ -0,0 +1,15 @@
```ts
const emailIdentity = user.identities.email
// Email address the the user used to sign up, e.g. "fluffyllama@app.com".
emailIdentity.id
// `true` if the user has verified their email address.
emailIdentity.isEmailVerified
// Datetime when the email verification email was sent.
emailIdentity.emailVerificationSentAt
// Datetime when the last password reset email was sent.
emailIdentity.passwordResetSentAt
```

View File

@ -1,47 +0,0 @@
The `getEmail` helper returns the user's email or `null` if the user doesn't have an email auth identity.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getEmail } from 'wasp/auth'
const MainPage = ({ user }) => {
const email = getEmail(user)
// ...
}
```
```js title=src/tasks.js
import { getEmail } from 'wasp/auth'
export const createTask = async (args, context) => {
const email = getEmail(context.user)
// ...
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getEmail, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const email = getEmail(user)
// ...
}
```
```ts title=src/tasks.ts
import { getEmail } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const email = getEmail(context.user)
// ...
}
```
</TabItem>
</Tabs>

View File

@ -1,47 +0,0 @@
The `getUsername` helper returns the user's username or `null` if the user doesn't have a username auth identity.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getUsername } from 'wasp/auth'
const MainPage = ({ user }) => {
const username = getUsername(user)
// ...
}
```
```js title=src/tasks.js
import { getUsername } from 'wasp/auth'
export const createTask = async (args, context) => {
const username = getUsername(context.user)
// ...
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getUsername, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const username = getUsername(user)
// ...
}
```
```ts title=src/tasks.ts
import { getUsername } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const username = getUsername(context.user)
// ...
}
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,6 @@
```ts
const githubIdentity = user.identities.github
// GitHub User ID for example "12345678"
githubIdentity.id
```

View File

@ -0,0 +1,6 @@
```ts
const googleIdentity = user.identities.google
// Google User ID for example "123456789012345678901"
googleIdentity.id
```

View File

@ -0,0 +1,6 @@
```ts
const keycloakIdentity = user.identities.keycloak
// Keycloak User ID for example "12345678-1234-1234-1234-123456789012"
keycloakIdentity.id
```

View File

@ -0,0 +1,6 @@
```ts
const usernameIdentity = user.identities.username
// Username that the user used to sign up, e.g. "fluffyllama"
usernameIdentity.id
```

View File

@ -1,14 +1,454 @@
---
title: Auth Entities
title: Accessing User Data
---
import ImgWithCaption from '@site/blog/components/ImgWithCaption'
import { Internal } from '@site/src/components/Tag'
import MultipleIdentitiesWarning from '../\_multiple-identities-warning.md';
import GetEmail from './\_get-email.md';
import GetUsername from './\_get-username.md';
import UsernameData from './\_username-data.md';
import EmailData from './\_email-data.md';
import GoogleData from './\_google-data.md';
import GithubData from './\_github-data.md';
import KeycloakData from './\_keycloak-data.md';
Wasp supports multiple different authentication methods and for each method, we need to store different information about the user. For example, if you are using the [Username & password](./username-and-pass) authentication method, we need to store the user's username and password. On the other hand, if you are using the [Email](./email) authentication method, you will need to store the user's email, password and for example, their email verification status.
First, we'll check out the most practical info: **how to access the user's data in your app**.
Then, we'll dive into the details of the **auth entities** that Wasp creates behind the scenes to store the user's data. For auth each method, Wasp needs to store different information about the user. For example, username for [Username & password](./username-and-pass) auth, email verification status for [Email](./email) auth, and so on.
We'll also show you how you can use these entities to create a custom signup action.
## Accessing the Auth Fields
When you receive the `user` object [on the client or the server](../overview.md#accessing-the-logged-in-user), it will contain all the user fields you defined in the `User` entity in your Wasp file. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the `username` or the email verification status. In Wasp, this data is called the `AuthUser` object.
### `AuthUser` Object Fields
All the `User` fields you defined will be present at the top level of the `AuthUser` object. The auth-related fields will be on the `identities` object. For each auth method you enable, there will be a separate data object in the `identities` object.
The `AuthUser` object will change depending on which auth method you have enabled in the Wasp file. For example, if you enabled the email auth and Google auth, it would look something like this:
<Tabs>
<TabItem value="google" label="User Signed Up with Google">
If the user has only the Google identity, the `AuthUser` object will look like this:
```ts
const user = {
// User data
id: 'cluqs9qyh00007cn73apj4hp7',
address: 'Some address',
// Auth methods specific data
identities: {
email: null,
google: {
id: '1117XXXX1301972049448',
},
},
}
```
</TabItem>
<TabItem value="email" label="User Signed Up with Email">
If the user has only the email identity, the `AuthUser` object will look like this:
```ts
const user = {
// User data
id: 'cluqsex9500017cn7i2hwsg17',
address: 'Some address',
// Auth methods specific data
identities: {
email: {
id: 'user@app.com',
isEmailVerified: true,
emailVerificationSentAt: '2024-04-08T10:06:02.204Z',
passwordResetSentAt: null,
},
google: null,
},
}
```
</TabItem>
</Tabs>
In the examples above, you can see the `identities` object contains the `email` and `google` objects. The `email` object contains the email-related data and the `google` object contains the Google-related data.
:::info Make sure to check if the data exists
Before accessing some auth method's data, you'll need to check if that data exists for the user and then access it:
```ts
if (user.identities.google !== null) {
const userId = user.identities.google.id
// ...
}
```
You need to do this because if a user didn't sign up with some auth method, the data for that auth method will be `null`.
:::
Let's look at the data for each of the available auth methods:
- [Username & password](../username-and-pass.md) data
<UsernameData />
- [Email](../email.md) data
<EmailData />
- [Google](../social-auth/google.md) data
<GoogleData />
- [GitHub](../social-auth/github.md) data
<GithubData />
- [Keycloak](../social-auth/keycloak.md) data
<KeycloakData />
If you support multiple auth methods, you'll need to find which identity exists for the user and then access its data:
```ts
if (user.identities.email !== null) {
const email = user.identities.email.id
// ...
} else if (user.identities.google !== null) {
const googleId = user.identities.google.id
// ...
}
```
### `getFirstProviderUserId` Helper
The `getFirstProviderUserId` method returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID.
This can be useful if you support multiple authentication methods and you need _any_ ID that identifies the user in your app.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
const MainPage = ({ user }) => {
const userId = user.getFirstProviderUserId()
// ...
}
```
```js title=src/tasks.js
export const createTask = async (args, context) => {
const userId = context.user.getFirstProviderUserId()
// ...
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { type AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const userId = user.getFirstProviderUserId()
// ...
}
```
```ts title=src/tasks.ts
export const createTask: CreateTask<...> = async (args, context) => {
const userId = context.user.getFirstProviderUserId()
// ...
}
```
</TabItem>
</Tabs>
<small>
\* Multiple identities per user will be possible in the future and then the `getFirstProviderUserId` method will return the ID of the first identity that it finds without any guarantees about which one it will be.
</small>
## Including the User with Other Entities
Sometimes, you might want to include the user's data when fetching other entities. For example, you might want to include the user's data with the tasks they have created.
We'll mention the `auth` and the `identities` relations which we will explain in more detail later in the [Entities Explained](#entities-explained) section.
:::caution Be careful about sensitive data
You'll need to include the `auth` and the `identities` relations to get the full auth data about the user. However, you should keep in mind that the `providerData` field in the `identities` can contain sensitive data like the user's hashed password (in case of email or username auth), so you will likely want to exclude it if you are returning those values to the client.
:::
You can include the full user's data with other entities using the `include` option in the Prisma queries:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/tasks.js"
export const getAllTasks = async (args, context) => {
return context.entities.Task.findMany({
orderBy: { id: 'desc' },
select: {
id: true,
title: true,
// highlight-next-line
user: {
include: {
// highlight-next-line
auth: {
include: {
// highlight-next-line
identities: {
// Including only the `providerName` and `providerUserId` fields
select: {
providerName: true,
providerUserId: true,
},
},
},
},
},
},
},
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/tasks.ts"
export const getAllTasks = (async (args, context) => {
return context.entities.Task.findMany({
orderBy: { id: 'desc' },
select: {
id: true,
title: true,
// highlight-next-line
user: {
include: {
// highlight-next-line
auth: {
include: {
// highlight-next-line
identities: {
// Including only the `providerName` and `providerUserId` fields
select: {
providerName: true,
providerUserId: true,
},
},
},
},
},
},
},
})
}) satisfies tasks.GetAllQuery<{}, {}>
```
</TabItem>
</Tabs>
If you have some **piece of the auth data that you want to access frequently** (for example the `username`), it's best to store it at the top level of the `User` entity.
For example, save the `username` or `email` as a property on the `User` and you'll be able to access it without including the `auth` and `identities` fields. We show an example in the [Defining Extra Fields on the User Entity](../overview.md#1-defining-extra-fields) section of the docs.
### Getting Auth Data from the User Object
When you have the `user` object with the `auth` and `identities` fields, it can be a bit tedious to obtain the auth data (like username or Google ID) from it:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {task.user.auth?.identities[0].providerUserId}
</div>
))}
</div>
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {task.user.auth?.identities[0].providerUserId}
</div>
))}
</div>
)
}
```
</TabItem>
</Tabs>
Wasp offers a few helper methods to access the user's auth data when you retrieve the `user` like this. They are `getUsername`, `getEmail` and `getFirstProviderUserId`. They can be used both on the client and the server.
#### `getUsername`
It accepts the `user` object and if the user signed up with the [Username & password](./username-and-pass) auth method, it returns the username or `null` otherwise. The `user` object needs to have the `auth` and the `identities` relations included.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getUsername } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getUsername(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getUsername } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getUsername(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
</Tabs>
#### `getEmail`
It accepts the `user` object and if the user signed up with the [Email](./email) auth method, it returns the email or `null` otherwise. The `user` object needs to have the `auth` and the `identities` relations included.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getEmail } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getEmail(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getEmail } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getEmail(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
</Tabs>
#### `getFirstProviderUserId`
It returns the first user ID that it finds for the user. For example if the user has signed up with email, it will return the email. If the user has signed up with Google, it will return the Google ID. The `user` object needs to have the `auth` and the `identities` relations included.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getFirstProviderUserId } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getFirstProviderUserId(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getFirstProviderUserId } from 'wasp/auth'
function MainPage() {
// ...
return (
<div className="tasks">
{tasks.map((task) => (
<div key={task.id} className="task">
{task.title} by {getFirstProviderUserId(task.user)}
</div>
))}
</div>
)
}
```
</TabItem>
</Tabs>
## Entities Explained
@ -16,7 +456,7 @@ To store user information, Wasp creates a few entities behind the scenes. In thi
### User Entity
When you want to add authentication to your app, you need to specify the user entity e.g. `User` in your Wasp file. This entity is a "business logic user" which represents a user of your app.
When you want to add authentication to your app, you need to specify the user entity e.g. `User` in your Wasp file. This entity is a "business logic user" which represents a user of your app.
You can use this entity to store any information about the user that you want to store. For example, you might want to store the user's name or address. You can also use the user entity to define the relations between users and other entities in your app. For example, you might want to define a relation between a user and the tasks that they have created.
@ -36,16 +476,18 @@ On the other hand, the `Auth`, `AuthIdentity` and `Session` entities are created
In the case you want to create a custom signup action, you will need to use the `Auth` and `AuthIdentity` entities directly.
### Example App Model
Let's imagine we created a simple tasks management app:
- The app has email and Google-based auth.
- Users can create tasks and see the tasks that they have created.
- The app has email and Google-based auth.
- Users can create tasks and see the tasks that they have created.
Let's look at how would that look in the database:
<ImgWithCaption alt="Example of Auth Entities" source="img/auth-entities/model-example.png" caption="Example of Auth Entities"/>
If we take a look at an example user in the database, we can see:
- The business logic user, `User` is connected to multiple `Task` entities.
- In this example, "Example User" has two tasks.
- The `User` is connected to exactly one `Auth` entity.
@ -65,7 +507,7 @@ entity Auth {=psl
id String @id @default(uuid())
userId Int? @unique
// Wasp injects this relation on the User entity as well
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
identities AuthIdentity[]
sessions Session[]
psl=}
@ -93,11 +535,12 @@ entity AuthIdentity {=psl
authId String
auth Auth @relation(fields: [authId], references: [id], onDelete: Cascade)
@@id([providerName, providerUserId])
@@id([providerName, providerUserId])
psl=}
```
The `AuthIdentity` fields:
- `providerName` is the name of the authentication provider.
- For example, `email` or `google`.
- `providerUserId` is the user's ID in the authentication provider.
@ -125,162 +568,13 @@ psl=}
```
The `Session` fields:
- `id` is a unique identifier of the `Session` entity.
- `expiresAt` is the date when the session expires.
- `userId` is a foreign key to the `Auth` entity.
- It is used to connect the `Session` entity with the `Auth` entity.
- `auth` is a relation to the `Auth` entity.
## Accessing the Auth Fields
If you are looking to access the user's email or username in your code, you can do that by accessing the info about the user that is stored in the `AuthIdentity` entity.
Everywhere where Wasp gives you the `user` object, it also includes the `auth` relation with the `identities` relation. This means that you can access the auth identity info by using the `user.auth.identities` array.
To make things a bit easier for you, Wasp offers a few helper functions that you can use to access the auth identity info.
### `getEmail`
<GetEmail />
### `getUsername`
<GetUsername />
### `getFirstProviderUserId`
The `getFirstProviderUserId` helper returns the first user ID (e.g. `username` or `email`) that it finds for the user or `null` if it doesn't find any.
[As mentioned before](#authidentity-entity-), the `providerUserId` field is how providers identify our users. For example, the user's `username` in the case of the username auth or the user's `email` in the case of the email auth. This can be useful if you support multiple authentication methods and you need *any* ID that identifies the user in your app.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { getFirstProviderUserId } from 'wasp/auth'
const MainPage = ({ user }) => {
const userId = getFirstProviderUserId(user)
// ...
}
```
```js title=src/tasks.js
import { getFirstProviderUserId } from 'wasp/auth'
export const createTask = async (args, context) => {
const userId = getFirstProviderUserId(context.user)
// ...
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { getFirstProviderUserId, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const userId = getFirstProviderUserId(user)
// ...
}
```
```ts title=src/tasks.ts
import { getFirstProviderUserId } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const userId = getFirstProviderUserId(context.user)
// ...
}
```
</TabItem>
</Tabs>
### `findUserIdentity`
You can find a specific auth identity by using the `findUserIdentity` helper function. This function takes a `user` and a `providerName` and returns the first `providerName` identity that it finds or `null` if it doesn't find any.
Possible provider names are:
- `email`
- `username`
- `google`
- `github`
This can be useful if you want to check if the user has a specific auth identity. For example, you might want to check if the user has an email auth identity or Google auth identity.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/MainPage.jsx"
import { findUserIdentity } from 'wasp/auth'
const MainPage = ({ user }) => {
const emailIdentity = findUserIdentity(user, 'email')
const googleIdentity = findUserIdentity(user, 'google')
if (emailIdentity) {
// ...
} else if (googleIdentity) {
// ...
}
// ...
}
```
```js title=src/tasks.js
import { findUserIdentity } from 'wasp/client/auth'
export const createTask = async (args, context) => {
const emailIdentity = findUserIdentity(context.user, 'email')
const googleIdentity = findUserIdentity(context.user, 'google')
if (emailIdentity) {
// ...
} else if (googleIdentity) {
// ...
}
// ...
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/MainPage.tsx"
import { findUserIdentity, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const emailIdentity = findUserIdentity(user, 'email')
const googleIdentity = findUserIdentity(user, 'google')
if (emailIdentity) {
// ...
} else if (googleIdentity) {
// ...
}
// ...
}
```
```ts title=src/tasks.ts
import { findUserIdentity } from 'wasp/client/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const emailIdentity = findUserIdentity(context.user, 'email')
const googleIdentity = findUserIdentity(context.user, 'google')
if (emailIdentity) {
// ...
} else if (googleIdentity) {
// ...
}
// ...
}
```
</TabItem>
</Tabs>
## Custom Signup Action
Let's take a look at how you can use the `Auth` and `AuthIdentity` entities to create custom login and signup actions. For example, you might want to create a custom signup action that creates a user in your app and also creates a user in a third-party service.
@ -304,7 +598,6 @@ action customSignup {
}
```
```js title="src/auth/signup.js"
import {
createProviderId,
@ -326,7 +619,7 @@ export const signup = async (args, { entities: { User } }) => {
providerId,
providerData,
// Any additional data you want to store on the User entity
{},
{}
)
// This is equivalent to:
@ -361,6 +654,7 @@ export const signup = async (args, { entities: { User } }) => {
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
@ -407,7 +701,7 @@ export const signup: CustomSignup<
providerId,
providerData,
// Any additional data you want to store on the User entity
{},
{}
)
// This is equivalent to:
@ -442,7 +736,8 @@ export const signup: CustomSignup<
}
}
```
</TabItem>
</Tabs>
You can use whichever method suits your needs better: either the `createUser` function or Prisma's `User.create` method. The `createUser` function is a bit more convenient to use because it hides some of the complexity. On the other hand, the `User.create` method gives you more control over the data that is stored in the `Auth` and `AuthIdentity` entities.
You can use whichever method suits your needs better: either the `createUser` function or Prisma's `User.create` method. The `createUser` function is a bit more convenient to use because it hides some of the complexity. On the other hand, the `User.create` method gives you more control over the data that is stored in the `Auth` and `AuthIdentity` entities.

View File

@ -153,26 +153,22 @@ const LogoutButton = () => {
You can get access to the `user` object both on the server and on the client. The `user` object contains the logged-in user's data.
The `user` object has all the fields that you defined in your `User` entity, plus the `auth` field which contains the auth identities connected to the user. For example, if the user signed up with their email, the `user` object might look something like this:
The `user` object has all the fields that you defined in your `User` entity. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the `username` or the email verification status. For example, if you have a user that signed up using an email and password, the `user` object might look like this:
```js
```ts
const user = {
id: "19c7d164-b5cb-4dde-a0cc-0daea77cf854",
// User data
id: "cluqsex9500017cn7i2hwsg17",
address: "Some address",
// Your entity's fields.
address: "My address",
// ...
// Auth identities connected to the user.
auth: {
id: "26ab6f96-ed76-4ee5-9ac3-2fd0bf19711f",
identities: [
{
providerName: "email",
providerUserId: "some@email.com",
providerData: { ... },
},
]
// Auth methods specific data
identities: {
email: {
id: "user@app.com",
isEmailVerified: true,
emailVerificationSentAt: "2024-04-08T10:06:02.204Z",
passwordResetSentAt: null,
},
},
}
```
@ -946,7 +942,7 @@ A dictionary of auth methods enabled for the app.
#### `onAuthFailedRedirectTo: String` <Required />
The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has `authRequired: true`).
Check out these [essentials docs on auth](../tutorial/auth#adding-auth-to-the-project) to see an example of usage.
Check out these [essential docs on auth](../tutorial/auth#adding-auth-to-the-project) to see an example of usage.
#### `onAuthSucceededRedirectTo: String`

View File

@ -7,4 +7,4 @@ The reference shows how to define both.
For behavior common to all providers, check the general [API Reference](/auth/overview.md#api-reference).
<!-- This snippet is used in google.md and github.md -->
<!-- This snippet is used in {google,github,keycloak}.md -->

View File

@ -1,3 +1,3 @@
When a user **signs in for the first time**, Wasp creates a new user account and links it to the chosen auth provider account for future logins.
<!-- This snippet is used in overview.md, google.md and github.md -->
<!-- This snippet is used in {overview,google,github,keycloak}.md -->

View File

@ -1,3 +1,3 @@
Wasp automatically generates the `defineUserSignupFields` function to help you correctly type your `userSignupFields` object.
<!-- This snippet is used in overview.md, google.md and github.md -->
<!-- This snippet is used in {overview,google,github,keycloak}.md -->

View File

@ -7,4 +7,4 @@ Wasp also lets you customize the configuration of the providers' settings using
Let's use this example to show both fields in action:
<!-- This snippet is used in google.md and github.md -->
<!-- This snippet is used in {google,github,keycloak}.md -->

View File

@ -7,4 +7,4 @@ There are two mechanisms used for overriding the default behavior:
Let's explore them in more detail.
<!-- This snippet is used in google.md and github.md -->
<!-- This snippet is used in {google,github,keycloak}.md -->

View File

@ -1,3 +1,3 @@
To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on [using auth](../../auth/overview).
<!-- This snippet is used in google.md and github.md -->
<!-- This snippet is used in {google,github,keycloak}.md -->

View File

@ -11,6 +11,8 @@ import WaspFileStructureNote from './\_wasp-file-structure-note.md';
import GetUserFieldsType from './\_getuserfields-type.md';
import ApiReferenceIntro from './\_api-reference-intro.md';
import UserSignupFieldsExplainer from '../\_user-signup-fields-explainer.md';
import GithubData from '../entities/_github-data.md';
import AccessingUserDataNote from '../\_accessing-user-data-note.md';
Wasp supports Github Authentication out of the box.
GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.
@ -469,6 +471,12 @@ export function getConfig() {
<UsingAuthNote />
When you receive the `user` object [on the client or the server](../overview.md#accessing-the-logged-in-user), you'll be able to access the user's GitHub ID like this:
<GithubData />
<AccessingUserDataNote />
## API Reference
<ApiReferenceIntro />

View File

@ -11,6 +11,8 @@ import WaspFileStructureNote from './\_wasp-file-structure-note.md';
import GetUserFieldsType from './\_getuserfields-type.md';
import ApiReferenceIntro from './\_api-reference-intro.md';
import UserSignupFieldsExplainer from '../\_user-signup-fields-explainer.md';
import GoogleData from '../entities/_google-data.md';
import AccessingUserDataNote from '../\_accessing-user-data-note.md';
Wasp supports Google Authentication out of the box.
Google Auth is arguably the best external auth option, as most users on the web already have Google accounts.
@ -495,6 +497,12 @@ export function getConfig() {
<UsingAuthNote />
When you receive the `user` object [on the client or the server](../overview.md#accessing-the-logged-in-user), you'll be able to access the user's Google ID like this:
<GoogleData />
<AccessingUserDataNote />
## API Reference
<ApiReferenceIntro />

View File

@ -11,6 +11,8 @@ import WaspFileStructureNote from './\_wasp-file-structure-note.md';
import GetUserFieldsType from './\_getuserfields-type.md';
import ApiReferenceIntro from './\_api-reference-intro.md';
import UserSignupFieldsExplainer from '../\_user-signup-fields-explainer.md';
import KeycloakData from '../entities/_keycloak-data.md';
import AccessingUserDataNote from '../\_accessing-user-data-note.md';
Wasp supports Keycloak Authentication out of the box.
@ -454,6 +456,12 @@ export function getConfig() {
<UsingAuthNote />
When you receive the `user` object [on the client or the server](../overview.md#accessing-the-logged-in-user), you'll be able to access the user's Keycloak ID like this:
<KeycloakData />
<AccessingUserDataNote />
## API Reference
<ApiReferenceIntro />

View File

@ -5,11 +5,12 @@ title: Username & Password
import { Required } from '@site/src/components/Tag';
import MultipleIdentitiesWarning from './\_multiple-identities-warning.md';
import ReadMoreAboutAuthEntities from './\_read-more-about-auth-entities.md';
import GetUsername from './entities/\_get-username.md';
import UserSignupFieldsExplainer from './\_user-signup-fields-explainer.md';
import UserFieldsExplainer from './\_user-fields.md';
import UsernameData from './entities/\_username-data.md';
import AccessingUserDataNote from './\_accessing-user-data-note.md';
Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.
Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client side.
## Setting Up Username & Password Authentication
@ -609,13 +610,11 @@ We suggest using the built-in field validators for your authentication flow. You
To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [auth overview docs](../auth/overview).
### `getUsername`
When you receive the `user` object [on the client or the server](./overview.md#accessing-the-logged-in-user), you'll be able to access the user's username like this:
If you are looking to access the user's username in your code, you can do that by accessing the info about the user that is stored in the `user.auth.identities` array.
<UsernameData />
To make things a bit easier for you, Wasp offers the `getUsername` helper.
<GetUsername />
<AccessingUserDataNote />
## API Reference

View File

@ -160,7 +160,7 @@ Auth field customization is no longer possible using the `_waspCustomValidations
:::
You can read more about the new auth system in the [Auth Entities](./auth/entities) section.
You can read more about the new auth system in the [Accessing User Data](./auth/entities) section.
## How to Migrate?
@ -642,7 +642,8 @@ You should see the new `Auth`, `AuthIdentity` and `Session` tables in your datab
Instead, you can now use `getUsername(user)` to get the username obtained from Username & Password auth method, or `getEmail(user)` to get the email obtained from Email auth method.
Read more about the helpers in the [Auth Entities - Accessing the Auth Fields](auth/entities#accessing-the-auth-fields) section.
Read more about the helpers in the [Accessing User Data](auth/entities#accessing-the-auth-fields) section.
1. Finally, **check that your app now fully works as it worked before**. If all the above steps were done correctly, everything should be working now.
:::info Migrating a deployed app

View File

@ -0,0 +1,267 @@
---
title: Migration from 0.13.X to 0.14.X
---
:::note Are you on 0.11.X or earlier?
This guide only covers the migration from **0.13.X to 0.14.X**. If you are migrating from 0.11.X or earlier, please read the [migration guide from 0.11.X to 0.12.X](./migrate-from-0-11-to-0-12.md) first.
:::
## What's new in 0.14.0?
### Better auth user API
We introduced a much simpler API for accessing user auth fields like `username`, `email` or `isEmailVerified` on the `user` object. You don't need to use helper functions every time you want to access the user's `username` or extra steps to get proper typing.
## How to migrate?
### Migrate how you access user auth fields
We had to make a couple of breaking changes to reach the new simpler API.
Follow the steps below to migrate:
1. **Replace the `getUsername` helper** with `user.identities.username.id`
If you didn't use the `getUsername` helper in your code, you can skip this step.
This helper changed and it no longer works with the `user` you receive as a prop on a page or through the `context`. You'll need to replace it with `user.identities.username.id`.
<Tabs>
<TabItem value="before" label="Before">
```tsx title="src/MainPage.tsx"
import { getUsername, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const username = getUsername(user)
// ...
}
```
```ts title=src/tasks.ts
import { getUsername } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const username = getUsername(context.user)
// ...
}
```
</TabItem>
<TabItem value="after" label="After">
```tsx title="src/MainPage.tsx"
import { AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const username = user.identities.username?.id
// ...
}
```
```ts title=src/tasks.ts
export const createTask: CreateTask<...> = async (args, context) => {
const username = context.user.identities.username?.id
// ...
}
```
</TabItem>
</Tabs>
1. **Replace the `getEmail` helper** with `user.identities.email.id`
If you didn't use the `getEmail` helper in your code, you can skip this step.
This helper changed and it no longer works with the `user` you receive as a prop on a page or through the `context`. You'll need to replace it with `user.identities.email.id`.
<Tabs>
<TabItem value="before" label="Before">
```tsx title="src/MainPage.tsx"
import { getEmail, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const email = getEmail(user)
// ...
}
```
```ts title=src/tasks.ts
import { getEmail } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const email = getEmail(context.user)
// ...
}
```
</TabItem>
<TabItem value="after" label="After">
```tsx title="src/MainPage.tsx"
import { AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const email = user.identities.email?.id
// ...
}
```
```ts title=src/tasks.ts
export const createTask: CreateTask<...> = async (args, context) => {
const email = context.user.identities.email?.id
// ...
}
```
</TabItem>
</Tabs>
1. **Replace accessing `providerData`** with `user.identities.<provider>.<value>`
If you didn't use any data from the `providerData` object, you can skip this step.
Replace `<provider>` with the provider name (for example `username`, `email`, `google`, `github`, etc.) and `<value>` with the field you want to access (for example `isEmailVerified`).
<Tabs>
<TabItem value="before" label="Before">
```tsx title="src/MainPage.tsx"
import { findUserIdentity, AuthUser } from 'wasp/auth'
function getProviderData(user: AuthUser) {
const emailIdentity = findUserIdentity(user, 'email')
// We needed this before check for proper type support
return emailIdentity && 'isEmailVerified' in emailIdentity.providerData
? emailIdentity.providerData
: null
}
const MainPage = ({ user }: { user: AuthUser }) => {
const providerData = getProviderData(user)
const isEmailVerified = providerData ? providerData.isEmailVerified : null
// ...
}
```
</TabItem>
<TabItem value="after" label="After">
```tsx title="src/MainPage.tsx"
import { AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
// The email object is properly typed, so we can access `isEmailVerified` directly
const isEmailVerified = user.identities.email?.isEmailVerified
// ...
}
```
</TabItem>
</Tabs>
1. **Use `getFirstProviderUserId` directly** on the user object
If you didn't use `getFirstProviderUserId` in your code, you can skip this step.
You should replace `getFirstProviderUserId(user)` with `user.getFirstProviderUserId()`.
<Tabs>
<TabItem value="before" label="Before">
```tsx title="src/MainPage.tsx"
import { getFirstProviderUserId, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const userId = getFirstProviderUserId(user)
// ...
}
```
```ts title=src/tasks.ts
import { getFirstProviderUserId } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const userId = getFirstProviderUserId(context.user)
// ...
}
```
</TabItem>
<TabItem value="after" label="After">
```tsx title="src/MainPage.tsx"
import { AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const userId = user.getFirstProviderUserId()
// ...
}
```
```ts title=src/tasks.ts
export const createTask: CreateTask<...> = async (args, context) => {
const userId = user.getFirstProviderUserId()
// ...
}
```
</TabItem>
</Tabs>
1. **Replace `findUserIdentity`** with checks on `user.identities.<provider>`
If you didn't use `findUserIdentity` in your code, you can skip this step.
Instead of using `findUserIdentity` to get the identity object, you can directly check if the identity exists on the `identities` object.
<Tabs>
<TabItem value="before" label="Before">
```tsx title="src/MainPage.tsx"
import { findUserIdentity, AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
const usernameIdentity = findUserIdentity(user, 'username')
if (usernameIdentity) {
// ...
}
}
```
```ts title=src/tasks.ts
import { findUserIdentity } from 'wasp/auth'
export const createTask: CreateTask<...> = async (args, context) => {
const usernameIdentity = findUserIdentity(context.user, 'username')
if (usernameIdentity) {
// ...
}
}
```
</TabItem>
<TabItem value="after" label="After">
```tsx title="src/MainPage.tsx"
import { AuthUser } from 'wasp/auth'
const MainPage = ({ user }: { user: AuthUser }) => {
if (user.identities.username) {
// ...
}
}
```
```ts title=src/tasks.ts
export const createTask: CreateTask<...> = async (args, context) => {
if (context.user.identities.username) {
// ...
}
}
```
</TabItem>
</Tabs>
That's it!
You should now be able to run your app with the new Wasp 0.14.0. We recommend reading through the updated [Accessing User Data](./auth/entities/entities.md) section to get a better understanding of the new API.

View File

@ -140,6 +140,7 @@ module.exports = {
'contact',
'migrate-from-0-11-to-0-12',
'migrate-from-0-12-to-0-13',
'migrate-from-0-13-to-0-14'
],
},
],