Remove generated files from git

This commit is contained in:
Filip Sodić 2024-01-31 15:45:43 +01:00 committed by Filip Sodić
parent 487a01fd74
commit 1a09d71672
21 changed files with 0 additions and 1406 deletions

View File

@ -1,108 +0,0 @@
import axios, { type AxiosError } from 'axios'
import config from 'wasp/core/config'
import { storage } from 'wasp/core/storage'
import { apiEventsEmitter } from './events.js'
// PUBLIC API
export const api = axios.create({
baseURL: config.apiUrl,
})
const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId'
let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined
// PRIVATE API (sdk)
export function setSessionId(sessionId: string): void {
waspAppAuthSessionId = sessionId
storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId)
apiEventsEmitter.emit('sessionId.set')
}
// PRIVATE API (sdk)
export function getSessionId(): string | undefined {
return waspAppAuthSessionId
}
// PRIVATE API (sdk)
export function clearSessionId(): void {
waspAppAuthSessionId = undefined
storage.remove(WASP_APP_AUTH_SESSION_ID_NAME)
apiEventsEmitter.emit('sessionId.clear')
}
// PRIVATE API (sdk)
export function removeLocalUserData(): void {
waspAppAuthSessionId = undefined
storage.clear()
apiEventsEmitter.emit('sessionId.clear')
}
api.interceptors.request.use((request) => {
const sessionId = getSessionId()
if (sessionId) {
request.headers['Authorization'] = `Bearer ${sessionId}`
}
return request
})
api.interceptors.response.use(undefined, (error) => {
if (error.response?.status === 401) {
clearSessionId()
}
return Promise.reject(error)
})
// This handler will run on other tabs (not the active one calling API functions),
// and will ensure they know about auth session ID changes.
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
// "Note: This won't work on the same page that is making the changes — it is really a way
// for other pages on the domain using the storage to sync any changes that are made."
window.addEventListener('storage', (event) => {
if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) {
if (!!event.newValue) {
waspAppAuthSessionId = event.newValue
apiEventsEmitter.emit('sessionId.set')
} else {
waspAppAuthSessionId = undefined
apiEventsEmitter.emit('sessionId.clear')
}
}
})
// PRIVATE API (sdk)
/**
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson)
} else {
// If any other error, we just propagate it.
throw error
}
}
class WaspHttpError extends Error {
statusCode: number
data: unknown
constructor (statusCode: number, message: string, data: unknown) {
super(message)
this.statusCode = statusCode
this.data = data
}
}

View File

@ -1,96 +0,0 @@
import { useState, createContext } from 'react'
import { createTheme } from '@stitches/react'
import { styled } from 'wasp/core/stitches.config'
import {
type State,
type CustomizationOptions,
type ErrorMessage,
type AdditionalSignupFields,
} from './types'
import { LoginSignupForm } from './internal/common/LoginSignupForm'
import { MessageError, MessageSuccess } from './internal/Message'
import { ForgotPasswordForm } from './internal/email/ForgotPasswordForm'
import { ResetPasswordForm } from './internal/email/ResetPasswordForm'
import { VerifyEmailForm } from './internal/email/VerifyEmailForm'
const logoStyle = {
height: '3rem'
}
const Container = styled('div', {
display: 'flex',
flexDirection: 'column',
})
const HeaderText = styled('h2', {
fontSize: '1.875rem',
fontWeight: '700',
marginTop: '1.5rem'
})
// PRIVATE API
export const AuthContext = createContext({
isLoading: false,
setIsLoading: (isLoading: boolean) => {},
setErrorMessage: (errorMessage: ErrorMessage | null) => {},
setSuccessMessage: (successMessage: string | null) => {},
})
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
state: State;
} & CustomizationOptions & {
additionalSignupFields?: AdditionalSignupFields;
}) {
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// TODO(matija): this is called on every render, is it a problem?
// If we do it in useEffect(), then there is a glitch between the default color and the
// user provided one.
const customTheme = createTheme(appearance ?? {})
const titles: Record<State, string> = {
login: 'Log in to your account',
signup: 'Create a new account',
"forgot-password": "Forgot your password?",
"reset-password": "Reset your password",
"verify-email": "Email verification",
}
const title = titles[state]
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
return (
<Container className={customTheme}>
<div>
{logo && (<img style={logoStyle} src={logo} alt='Your Company' />)}
<HeaderText>{title}</HeaderText>
</div>
{errorMessage && (
<MessageError>
{errorMessage.title}{errorMessage.description && ': '}{errorMessage.description}
</MessageError>
)}
{successMessage && <MessageSuccess>{successMessage}</MessageSuccess>}
<AuthContext.Provider value={{ isLoading, setIsLoading, setErrorMessage, setSuccessMessage }}>
{(state === 'login' || state === 'signup') && (
<LoginSignupForm
state={state}
socialButtonsDirection={socialButtonsDirection}
additionalSignupFields={additionalSignupFields}
/>
)}
{state === 'forgot-password' && (<ForgotPasswordForm />)}
{state === 'reset-password' && (<ResetPasswordForm />)}
{state === 'verify-email' && (<VerifyEmailForm />)}
</AuthContext.Provider>
</Container>
)
}
// PRIVATE API
export default Auth;

View File

@ -1,18 +0,0 @@
import Auth from './Auth'
import { type CustomizationOptions, State } from './types'
// PUBLIC API
export function LoginForm({
appearance,
logo,
socialLayout,
}: CustomizationOptions) {
return (
<Auth
appearance={appearance}
logo={logo}
socialLayout={socialLayout}
state={State.Login}
/>
)
}

View File

@ -1,24 +0,0 @@
import Auth from './Auth'
import {
type CustomizationOptions,
type AdditionalSignupFields,
State,
} from './types'
// PUBLIC API
export function SignupForm({
appearance,
logo,
socialLayout,
additionalFields,
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
return (
<Auth
appearance={appearance}
logo={logo}
socialLayout={socialLayout}
state={State.Signup}
additionalSignupFields={additionalFields}
/>
)
}

View File

@ -1,102 +0,0 @@
import { styled } from 'wasp/core/stitches.config'
// PRIVATE API
export const Form = styled('form', {
marginTop: '1.5rem',
})
// PUBLIC API
export const FormItemGroup = styled('div', {
'& + div': {
marginTop: '1.5rem',
},
})
// PUBLIC API
export const FormLabel = styled('label', {
display: 'block',
fontSize: '$sm',
fontWeight: '500',
marginBottom: '0.5rem',
})
const commonInputStyles = {
display: 'block',
lineHeight: '1.5rem',
fontSize: '$sm',
borderWidth: '1px',
borderColor: '$gray600',
backgroundColor: '#f8f4ff',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'&:focus': {
borderWidth: '1px',
borderColor: '$gray700',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
},
'&:disabled': {
opacity: 0.5,
cursor: 'not-allowed',
backgroundColor: '$gray400',
borderColor: '$gray400',
color: '$gray500',
},
borderRadius: '0.375rem',
width: '100%',
paddingTop: '0.375rem',
paddingBottom: '0.375rem',
paddingLeft: '0.75rem',
paddingRight: '0.75rem',
margin: 0,
}
// PUBLIC API
export const FormInput = styled('input', commonInputStyles)
// PUBLIC API
export const FormTextarea = styled('textarea', commonInputStyles)
// PUBLIC API
export const FormError = styled('div', {
display: 'block',
fontSize: '$sm',
fontWeight: '500',
color: '$formErrorText',
marginTop: '0.5rem',
})
// PRIVATE API
export const SubmitButton = styled('button', {
display: 'flex',
justifyContent: 'center',
width: '100%',
borderWidth: '1px',
borderColor: '$brand',
backgroundColor: '$brand',
color: '$submitButtonText',
padding: '0.5rem 0.75rem',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
fontWeight: '600',
fontSize: '$sm',
lineHeight: '1.25rem',
borderRadius: '0.375rem',
// TODO(matija): extract this into separate BaseButton component and then inherit it.
'&:hover': {
backgroundColor: '$brandAccent',
borderColor: '$brandAccent',
},
'&:disabled': {
opacity: 0.5,
cursor: 'not-allowed',
backgroundColor: '$gray400',
borderColor: '$gray400',
color: '$gray500',
},
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms',
})

View File

@ -1,21 +0,0 @@
import { styled } from 'wasp/core/stitches.config'
// PRIVATE API
export const Message = styled('div', {
padding: '0.5rem 0.75rem',
borderRadius: '0.375rem',
marginTop: '1rem',
background: '$gray400',
})
// PRIVATE API
export const MessageError = styled(Message, {
background: '$errorBackground',
color: '$errorText',
})
// PRIVATE API
export const MessageSuccess = styled(Message, {
background: '$successBackground',
color: '$successText',
})

View File

@ -1,184 +0,0 @@
import { useContext } from 'react'
import { useForm, UseFormReturn } from 'react-hook-form'
import { styled } from 'wasp/core/stitches.config'
import config from 'wasp/core/config'
import { AuthContext } from '../../Auth'
import {
Form,
FormInput,
FormItemGroup,
FormLabel,
FormError,
FormTextarea,
SubmitButton,
} from '../Form'
import type {
AdditionalSignupFields,
AdditionalSignupField,
AdditionalSignupFieldRenderFn,
FormState,
} from '../../types'
import { useHistory } from 'react-router-dom'
import { useEmail } from '../email/useEmail'
// PRIVATE API
export type LoginSignupFormFields = {
[key: string]: string;
}
// PRIVATE API
export const LoginSignupForm = ({
state,
socialButtonsDirection = 'horizontal',
additionalSignupFields,
}: {
state: 'login' | 'signup'
socialButtonsDirection?: 'horizontal' | 'vertical'
additionalSignupFields?: AdditionalSignupFields
}) => {
const {
isLoading,
setErrorMessage,
setSuccessMessage,
setIsLoading,
} = useContext(AuthContext)
const isLogin = state === 'login'
const cta = isLogin ? 'Log in' : 'Sign up';
const history = useHistory();
const onErrorHandler = (error) => {
setErrorMessage({ title: error.message, description: error.data?.data?.message })
};
const hookForm = useForm<LoginSignupFormFields>()
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
const { handleSubmit } = useEmail({
isLogin,
onError: onErrorHandler,
showEmailVerificationPending() {
hookForm.reset()
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
},
onLoginSuccess() {
history.push('/')
},
});
async function onSubmit (data) {
setIsLoading(true);
setErrorMessage(null);
setSuccessMessage(null);
try {
await handleSubmit(data);
} finally {
setIsLoading(false);
}
}
return (<>
<Form onSubmit={hookFormHandleSubmit(onSubmit)}>
<FormItemGroup>
<FormLabel>E-mail</FormLabel>
<FormInput
{...register('email', {
required: 'Email is required',
})}
type="email"
disabled={isLoading}
/>
{errors.email && <FormError>{errors.email.message}</FormError>}
</FormItemGroup>
<FormItemGroup>
<FormLabel>Password</FormLabel>
<FormInput
{...register('password', {
required: 'Password is required',
})}
type="password"
disabled={isLoading}
/>
{errors.password && <FormError>{errors.password.message}</FormError>}
</FormItemGroup>
<AdditionalFormFields
hookForm={hookForm}
formState={{ isLoading }}
additionalSignupFields={additionalSignupFields}
/>
<FormItemGroup>
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
</FormItemGroup>
</Form>
</>)
}
function AdditionalFormFields({
hookForm,
formState: { isLoading },
additionalSignupFields,
}: {
hookForm: UseFormReturn<LoginSignupFormFields>;
formState: FormState;
additionalSignupFields: AdditionalSignupFields;
}) {
const {
register,
formState: { errors },
} = hookForm;
function renderField<ComponentType extends React.JSXElementConstructor<any>>(
field: AdditionalSignupField,
// Ideally we would use ComponentType here, but it doesn't work with react-hook-form
Component: any,
props?: React.ComponentProps<ComponentType>
) {
return (
<FormItemGroup key={field.name}>
<FormLabel>{field.label}</FormLabel>
<Component
{...register(field.name, field.validations)}
{...props}
disabled={isLoading}
/>
{errors[field.name] && (
<FormError>{errors[field.name].message}</FormError>
)}
</FormItemGroup>
);
}
if (areAdditionalFieldsRenderFn(additionalSignupFields)) {
return additionalSignupFields(hookForm, { isLoading })
}
return (
additionalSignupFields &&
additionalSignupFields.map((field) => {
if (isFieldRenderFn(field)) {
return field(hookForm, { isLoading })
}
switch (field.type) {
case 'input':
return renderField<typeof FormInput>(field, FormInput, {
type: 'text',
})
case 'textarea':
return renderField<typeof FormTextarea>(field, FormTextarea)
default:
throw new Error(
`Unsupported additional signup field type: ${field.type}`
)
}
})
)
}
function isFieldRenderFn(
additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn
): additionalSignupField is AdditionalSignupFieldRenderFn {
return typeof additionalSignupField === 'function'
}
function areAdditionalFieldsRenderFn(
additionalSignupFields: AdditionalSignupFields
): additionalSignupFields is AdditionalSignupFieldRenderFn {
return typeof additionalSignupFields === 'function'
}

View File

@ -1,49 +0,0 @@
import { createTheme } from '@stitches/react'
import { UseFormReturn, RegisterOptions } from 'react-hook-form'
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
// PRIVATE API
export enum State {
Login = 'login',
Signup = 'signup',
ForgotPassword = 'forgot-password',
ResetPassword = 'reset-password',
VerifyEmail = 'verify-email',
}
// PUBLIC API
export type CustomizationOptions = {
logo?: string
socialLayout?: 'horizontal' | 'vertical'
appearance?: Parameters<typeof createTheme>[0]
}
// PRIVATE API
export type ErrorMessage = {
title: string
description?: string
}
// PRIVATE API
export type FormState = {
isLoading: boolean
}
// PRIVATE API
export type AdditionalSignupFieldRenderFn = (
hookForm: UseFormReturn<LoginSignupFormFields>,
formState: FormState
) => React.ReactNode
// PRIVATE API
export type AdditionalSignupField = {
name: string
label: string
type: 'input' | 'textarea'
validations?: RegisterOptions<LoginSignupFormFields>
}
// PRIVATE API
export type AdditionalSignupFields =
| (AdditionalSignupField | AdditionalSignupFieldRenderFn)[]
| AdditionalSignupFieldRenderFn

View File

@ -1,15 +0,0 @@
import { setSessionId } from 'wasp/client/api'
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'
// PRIVATE API
export async function initSession(sessionId: string): Promise<void> {
setSessionId(sessionId)
// We need to invalidate queries after login in order to get the correct user
// data in the React components (using `useAuth`).
// Redirects after login won't work properly without this.
// TODO(filip): We are currently removing all the queries, but we should
// remove only non-public, user-dependent queries - public queries are
// expected not to change in respect to the currently logged in user.
await invalidateAndRemoveQueries()
}

View File

@ -1,18 +0,0 @@
import { api, removeLocalUserData } from 'wasp/client/api'
import { invalidateAndRemoveQueries } from 'wasp/operations/resources'
// PUBLIC API
export default async function logout(): Promise<void> {
try {
await api.post('/auth/logout')
} finally {
// Even if the logout request fails, we still want to remove the local user data
// in case the logout failed because of a network error and the user walked away
// from the computer.
removeLocalUserData()
// TODO(filip): We are currently invalidating and removing all the queries, but
// we should remove only the non-public, user-dependent ones.
await invalidateAndRemoveQueries()
}
}

View File

@ -1,46 +0,0 @@
import type { Router, Request } from 'express'
import type { Prisma } from '@prisma/client'
import type { Expand } from 'wasp/universal/types'
import type { ProviderName } from '../utils'
// PUBLIC API
export function defineUserSignupFields(fields: UserSignupFields) {
return fields
}
type UserEntityCreateInput = Prisma.UserCreateInput
// PRIVATE API
export type ProviderConfig = {
// Unique provider identifier, used as part of URL paths
id: ProviderName;
displayName: string;
// Each provider config can have an init method which is ran on setup time
// e.g. for oAuth providers this is the time when the Passport strategy is registered.
init?(provider: ProviderConfig): Promise<InitData>;
// Every provider must have a setupRouter method which returns the Express router.
// In this function we are flexibile to do what ever is necessary to make the provider work.
createRouter(provider: ProviderConfig, initData: InitData): Router;
};
// PRIVATE API
export type InitData = {
[key: string]: any;
}
// PRIVATE API
export type RequestWithWasp = Request & { wasp?: { [key: string]: any } }
// PRIVATE API
export type PossibleUserFields = Expand<Partial<UserEntityCreateInput>>
// PRIVATE API
export type UserSignupFields = {
[key in keyof PossibleUserFields]: FieldGetter<
PossibleUserFields[key]
>
}
type FieldGetter<T> = (
data: { [key: string]: unknown }
) => Promise<T | undefined> | T | undefined

View File

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

View File

@ -1,40 +0,0 @@
import { deserialize as superjsonDeserialize } from 'superjson'
import { useQuery } from 'wasp/rpc'
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/types'
import type { AuthUser } from './types'
import { addMetadataToQuery } from 'wasp/rpc/queries'
// PUBLIC API
export const getMe = createUserGetter()
// PUBLIC API
export default function useAuth(queryFnArgs?: unknown, config?: any) {
return useQuery(getMe, queryFnArgs, config)
}
function createUserGetter() {
const getMeRelativePath = 'auth/me'
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
async function getMe(): Promise<AuthUser | null> {
try {
const response = await api.get(getMeRoute.path)
return superjsonDeserialize(response.data)
} catch (error) {
if (error.response?.status === 401) {
return null
} else {
handleApiError(error)
}
}
}
addMetadataToQuery(getMe, {
relativeQueryPath: getMeRelativePath,
queryRoute: getMeRoute,
entitiesUsed: ['User'],
})
return getMe
}

View File

@ -1,27 +0,0 @@
import type { AuthUser, ProviderName, DeserializedAuthIdentity } from './types'
// PUBLIC API
export function getEmail(user: AuthUser): string | null {
return findUserIdentity(user, "email")?.providerUserId ?? null;
}
// PUBLIC API
export function getUsername(user: AuthUser): string | null {
return findUserIdentity(user, "username")?.providerUserId ?? null;
}
// PUBLIC API
export function getFirstProviderUserId(user?: AuthUser): string | null {
if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
return null;
}
return user.auth.identities[0].providerUserId ?? null;
}
// PUBLIC API
export function findUserIdentity(user: AuthUser, providerName: ProviderName): DeserializedAuthIdentity | undefined {
return user.auth.identities.find(
(identity) => identity.providerName === providerName
);
}

View File

@ -1,321 +0,0 @@
import { hashPassword } from './password.js'
import { verify } from './jwt.js'
import { prisma, HttpError, AuthError } from 'wasp/server'
import { sleep } from 'wasp/server/utils'
import {
type User,
type Auth,
type AuthIdentity,
} from 'wasp/entities'
import { Prisma } from '@prisma/client';
import { throwValidationError } from './validation.js'
import { type UserSignupFields, type PossibleUserFields } from './providers/types.js'
// PRIVATE API
export type EmailProviderData = {
hashedPassword: string;
isEmailVerified: boolean;
emailVerificationSentAt: string | null;
passwordResetSentAt: string | null;
}
// PRIVATE API
export type UsernameProviderData = {
hashedPassword: string;
}
// PRIVATE API
export type OAuthProviderData = {}
// PRIVATE API
/**
* This type is used for type-level programming e.g. to enumerate
* all possible provider data types.
*
* The keys of this type are the names of the providers and the values
* are the types of the provider data.
*/
export type PossibleProviderData = {
email: EmailProviderData;
username: UsernameProviderData;
google: OAuthProviderData;
github: OAuthProviderData;
}
// PRIVATE API
export type ProviderName = keyof PossibleProviderData
// PRIVATE API
export const contextWithUserEntity = {
entities: {
User: prisma.user
}
}
// PRIVATE API
export const authConfig = {
failureRedirectPath: "/login",
successRedirectPath: "/",
}
// PRIVATE API
/**
* ProviderId uniquely identifies an auth identity e.g.
* "email" provider with user id "test@test.com" or
* "google" provider with user id "1234567890".
*
* We use this type to avoid passing the providerName and providerUserId
* separately. Also, we can normalize the providerUserId to make sure it's
* consistent across different DB operations.
*/
export type ProviderId = {
providerName: ProviderName;
providerUserId: string;
}
// PUBLIC API
export function createProviderId(providerName: ProviderName, providerUserId: string): ProviderId {
return {
providerName,
providerUserId: providerUserId.toLowerCase(),
}
}
// PUBLIC API
export async function findAuthIdentity(providerId: ProviderId): Promise<AuthIdentity | null> {
return prisma.authIdentity.findUnique({
where: {
providerName_providerUserId: providerId,
}
});
}
// PUBLIC API
/**
* Updates the provider data for the given auth identity.
*
* This function performs data sanitization and serialization.
* Sanitization is done by hashing the password, so this function
* expects the password received in the `providerDataUpdates`
* **not to be hashed**.
*/
export async function updateAuthIdentityProviderData<PN extends ProviderName>(
providerId: ProviderId,
existingProviderData: PossibleProviderData[PN],
providerDataUpdates: Partial<PossibleProviderData[PN]>,
): Promise<AuthIdentity> {
// We are doing the sanitization here only on updates to avoid
// hashing the password multiple times.
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
const newProviderData = {
...existingProviderData,
...sanitizedProviderDataUpdates,
}
const serializedProviderData = await serializeProviderData<PN>(newProviderData);
return prisma.authIdentity.update({
where: {
providerName_providerUserId: providerId,
},
data: { providerData: serializedProviderData },
});
}
type FindAuthWithUserResult = Auth & {
user: User
}
// PRIVATE API
export async function findAuthWithUserBy(
where: Prisma.AuthWhereInput
): Promise<FindAuthWithUserResult> {
return prisma.auth.findFirst({ where, include: { user: true }});
}
// PUBLIC API
export async function createUser(
providerId: ProviderId,
serializedProviderData?: string,
userFields?: PossibleUserFields,
): Promise<User & {
auth: Auth
}> {
return prisma.user.create({
data: {
// Using any here to prevent type errors when userFields are not
// defined. We want Prisma to throw an error in that case.
...(userFields ?? {} as any),
auth: {
create: {
identities: {
create: {
providerName: providerId.providerName,
providerUserId: providerId.providerUserId,
providerData: serializedProviderData,
},
},
}
},
},
// We need to include the Auth entity here because we need `authId`
// to be able to create a session.
include: {
auth: true,
},
})
}
// PRIVATE API
export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
return prisma.user.deleteMany({ where: { auth: {
id: authId,
} } })
}
// PRIVATE API
export async function verifyToken<T = unknown>(token: string): Promise<T> {
return verify(token);
}
// PRIVATE API
// If an user exists, we don't want to leak information
// about it. Pretending that we're doing some work
// will make it harder for an attacker to determine
// if a user exists or not.
// NOTE: Attacker measuring time to response can still determine
// if a user exists or not. We'll be able to avoid it when
// we implement e-mail sending via jobs.
export async function doFakeWork(): Promise<unknown> {
const timeToWork = Math.floor(Math.random() * 1000) + 1000;
return sleep(timeToWork);
}
// PRIVATE API
export function rethrowPossibleAuthError(e: unknown): void {
if (e instanceof AuthError) {
throwValidationError(e.message);
}
// Prisma code P2002 is for unique constraint violations.
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new HttpError(422, 'Save failed', {
message: `user with the same identity already exists`,
})
}
if (e instanceof Prisma.PrismaClientValidationError) {
// NOTE: Logging the error since this usually means that there are
// required fields missing in the request, we want the developer
// to know about it.
console.error(e)
throw new HttpError(422, 'Save failed', {
message: 'there was a database error'
})
}
// Prisma code P2021 is for missing table errors.
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2021') {
// NOTE: Logging the error since this usually means that the database
// migrations weren't run, we want the developer to know about it.
console.error(e)
console.info('🐝 This error can happen if you did\'t run the database migrations.')
throw new HttpError(500, 'Save failed', {
message: `there was a database error`,
})
}
// Prisma code P2003 is for foreign key constraint failure
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
console.error(e)
console.info(`🐝 This error can happen if you have some relation on your User entity
but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
throw new HttpError(500, 'Save failed', {
message: `there was a database error`,
})
}
throw e
}
// PRIVATE API
export async function validateAndGetUserFields(
data: {
[key: string]: unknown
},
userSignupFields?: UserSignupFields,
): Promise<Record<string, any>> {
const {
password: _password,
...sanitizedData
} = data;
const result: Record<string, any> = {};
if (!userSignupFields) {
return result;
}
for (const [field, getFieldValue] of Object.entries(userSignupFields)) {
try {
const value = await getFieldValue(sanitizedData)
result[field] = value
} catch (e) {
throwValidationError(e.message)
}
}
return result;
}
// PUBLIC API
export function deserializeAndSanitizeProviderData<PN extends ProviderName>(
providerData: string,
{ shouldRemovePasswordField = false }: { shouldRemovePasswordField?: boolean } = {},
): PossibleProviderData[PN] {
// NOTE: We are letting JSON.parse throw an error if the providerData is not valid JSON.
let data = JSON.parse(providerData) as PossibleProviderData[PN];
if (providerDataHasPasswordField(data) && shouldRemovePasswordField) {
delete data.hashedPassword;
}
return data;
}
// PUBLIC API
export async function sanitizeAndSerializeProviderData<PN extends ProviderName>(
providerData: PossibleProviderData[PN],
): Promise<string> {
return serializeProviderData(
await sanitizeProviderData(providerData)
);
}
function serializeProviderData<PN extends ProviderName>(providerData: PossibleProviderData[PN]): string {
return JSON.stringify(providerData);
}
async function sanitizeProviderData<PN extends ProviderName>(
providerData: PossibleProviderData[PN],
): Promise<PossibleProviderData[PN]> {
const data = {
...providerData,
};
if (providerDataHasPasswordField(data)) {
data.hashedPassword = await hashPassword(data.hashedPassword);
}
return data;
}
function providerDataHasPasswordField(
providerData: PossibleProviderData[keyof PossibleProviderData],
): providerData is { hashedPassword: string } {
return 'hashedPassword' in providerData;
}
// PRIVATE API
export function throwInvalidCredentialsError(message?: string): void {
throw new HttpError(401, 'Invalid credentials', { message })
}

View File

@ -1,83 +0,0 @@
import { HttpError } from 'wasp/server';
export const PASSWORD_FIELD = 'password';
const USERNAME_FIELD = 'username';
const EMAIL_FIELD = 'email';
const TOKEN_FIELD = 'token';
// PUBLIC API
export function ensureValidEmail(args: unknown): void {
validate(args, [
{ validates: EMAIL_FIELD, message: 'email must be present', validator: email => !!email },
{ validates: EMAIL_FIELD, message: 'email must be a valid email', validator: email => isValidEmail(email) },
]);
}
// PUBLIC API
export function ensureValidUsername(args: unknown): void {
validate(args, [
{ validates: USERNAME_FIELD, message: 'username must be present', validator: username => !!username }
]);
}
// PUBLIC API
export function ensurePasswordIsPresent(args: unknown): void {
validate(args, [
{ validates: PASSWORD_FIELD, message: 'password must be present', validator: password => !!password },
]);
}
// PUBLIC API
export function ensureValidPassword(args: unknown): void {
validate(args, [
{ validates: PASSWORD_FIELD, message: 'password must be at least 8 characters', validator: password => isMinLength(password, 8) },
{ validates: PASSWORD_FIELD, message: 'password must contain a number', validator: password => containsNumber(password) },
]);
}
// PUBLIC API
export function ensureTokenIsPresent(args: unknown): void {
validate(args, [
{ validates: TOKEN_FIELD, message: 'token must be present', validator: token => !!token },
]);
}
// PRIVATE API
export function throwValidationError(message: string): void {
throw new HttpError(422, 'Validation failed', { message })
}
function validate(args: unknown, validators: { validates: string, message: string, validator: (value: unknown) => boolean }[]): void {
for (const { validates, message, validator } of validators) {
if (!validator(args[validates])) {
throwValidationError(message);
}
}
}
// NOTE(miho): it would be good to replace our custom validations with e.g. Zod
const validEmailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
function isValidEmail(input: unknown): boolean {
if (typeof input !== 'string') {
return false
}
return input.match(validEmailRegex) !== null
}
function isMinLength(input: unknown, minLength: number): boolean {
if (typeof input !== 'string') {
return false
}
return input.length >= minLength
}
function containsNumber(input: unknown): boolean {
if (typeof input !== 'string') {
return false
}
return /\d/.test(input)
}

View File

@ -1,22 +0,0 @@
import { api, handleApiError } from 'wasp/client/api'
import { HttpMethod } from 'wasp/types'
import {
serialize as superjsonSerialize,
deserialize as superjsonDeserialize,
} from 'superjson'
export type OperationRoute = { method: HttpMethod, path: string }
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
try {
const superjsonArgs = superjsonSerialize(args)
const response = await api.post(operationRoute.path, superjsonArgs)
return superjsonDeserialize(response.data)
} catch (error) {
handleApiError(error)
}
}
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
}

View File

@ -1,99 +0,0 @@
import { type Expand } from 'wasp/universal/types';
import { type Request, type Response } from 'express'
import { type ParamsDictionary as ExpressParams, type Query as ExpressQuery } from 'express-serve-static-core'
import { prisma } from 'wasp/server'
import {
type User,
type Auth,
type AuthIdentity,
} from "wasp/entities"
import {
type EmailProviderData,
type UsernameProviderData,
type OAuthProviderData,
} from 'wasp/auth/utils'
import { type _Entity } from "./taggedEntities"
import { type Payload } from "./serialization";
export * from "./taggedEntities"
export * from "./serialization"
export type Query<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
Operation<Entities, Input, Output>
export type Action<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
Operation<Entities, Input, Output>
export type AuthenticatedQuery<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
AuthenticatedOperation<Entities, Input, Output>
export type AuthenticatedAction<Entities extends _Entity[], Input extends Payload, Output extends Payload> =
AuthenticatedOperation<Entities, Input, Output>
type AuthenticatedOperation<Entities extends _Entity[], Input extends Payload, Output extends Payload> = (
args: Input,
context: ContextWithUser<Entities>,
) => Output | Promise<Output>
export type AuthenticatedApi<
Entities extends _Entity[],
Params extends ExpressParams,
ResBody,
ReqBody,
ReqQuery extends ExpressQuery,
Locals extends Record<string, any>
> = (
req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>,
res: Response<ResBody, Locals>,
context: ContextWithUser<Entities>,
) => void
type Operation<Entities extends _Entity[], Input, Output> = (
args: Input,
context: Context<Entities>,
) => Output | Promise<Output>
export type Api<
Entities extends _Entity[],
Params extends ExpressParams,
ResBody,
ReqBody,
ReqQuery extends ExpressQuery,
Locals extends Record<string, any>
> = (
req: Request<Params, ResBody, ReqBody, ReqQuery, Locals>,
res: Response<ResBody, Locals>,
context: Context<Entities>,
) => void
type EntityMap<Entities extends _Entity[]> = {
[EntityName in Entities[number]["_entityName"]]: PrismaDelegate[EntityName]
}
export type PrismaDelegate = {
"User": typeof prisma.user,
"Task": typeof prisma.task,
}
type Context<Entities extends _Entity[]> = Expand<{
entities: Expand<EntityMap<Entities>>
}>
type ContextWithUser<Entities extends _Entity[]> = Expand<Context<Entities> & { user?: AuthUser }>
// TODO: This type must match the logic in auth/session.js (if we remove the
// password field from the object there, we must do the same here). Ideally,
// these two things would live in the same place:
// https://github.com/wasp-lang/wasp/issues/965
export type DeserializedAuthIdentity = Expand<Omit<AuthIdentity, 'providerData'> & {
providerData: Omit<EmailProviderData, 'password'> | Omit<UsernameProviderData, 'password'> | OAuthProviderData
}>
export type AuthUser = User & {
auth: Auth & {
identities: DeserializedAuthIdentity[]
} | null
}
export type { ProviderName } from 'wasp/auth/utils'

View File

@ -1,50 +0,0 @@
import { prisma } from 'wasp/server'
import { createTask as createTask_ext } from 'wasp/ext-src/task/actions.js'
import { updateTask as updateTask_ext } from 'wasp/ext-src/task/actions.js'
import { deleteTasks as deleteTasks_ext } from 'wasp/ext-src/task/actions.js'
import { send as send_ext } from 'wasp/ext-src/user/customEmailSending.js'
export type CreateTask = typeof createTask_ext
export const createTask = async (args, context) => {
return (createTask_ext as any)(args, {
...context,
entities: {
Task: prisma.task,
},
})
}
export type UpdateTask = typeof updateTask_ext
export const updateTask = async (args, context) => {
return (updateTask_ext as any)(args, {
...context,
entities: {
Task: prisma.task,
},
})
}
export type DeleteTasks = typeof deleteTasks_ext
export const deleteTasks = async (args, context) => {
return (deleteTasks_ext as any)(args, {
...context,
entities: {
Task: prisma.task,
},
})
}
export type CustomEmailSending = typeof send_ext
export const customEmailSending = async (args, context) => {
return (send_ext as any)(args, {
...context,
entities: {
User: prisma.user,
},
})
}

View File

@ -1,14 +0,0 @@
import { prisma } from 'wasp/server'
import { getTasks as getTasks_ext } from 'wasp/ext-src/task/queries.js'
export type GetTasks = typeof getTasks_ext
export const getTasks = async (args, context) => {
return (getTasks_ext as any)(args, {
...context,
entities: {
Task: prisma.task,
},
})
}

View File

@ -1,67 +0,0 @@
import crypto from 'crypto'
import { Request, Response, NextFunction } from 'express'
import { readdir } from 'fs'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { type AuthUser } from 'wasp/auth'
type RequestWithExtraFields = Request & {
user?: AuthUser;
sessionId?: string;
}
/**
* Decorator for async express middleware that handles promise rejections.
* @param {Func} middleware - Express middleware function.
* @returns Express middleware that is exactly the same as the given middleware but,
* if given middleware returns promise, reject of that promise will be correctly handled,
* meaning that error will be forwarded to next().
*/
export const handleRejection = (
middleware: (
req: RequestWithExtraFields,
res: Response,
next: NextFunction
) => any
) =>
async (req: RequestWithExtraFields, res: Response, next: NextFunction) => {
try {
await middleware(req, res, next)
} catch (error) {
next(error)
}
}
export const sleep = (ms: number): Promise<unknown> => new Promise((r) => setTimeout(r, ms))
export function getDirPathFromFileUrl(fileUrl: string): string {
return fileURLToPath(dirname(fileUrl))
}
export async function importJsFilesFromDir(
pathToDir: string,
whitelistedFileNames: string[] | null = null
): Promise<any[]> {
return new Promise((resolve, reject) => {
readdir(pathToDir, async (err, files) => {
if (err) {
return reject(err)
}
const importPromises = files
.filter((file) => file.endsWith('.js') && isWhitelistedFileName(file))
.map((file) => import(`${pathToDir}/${file}`))
resolve(Promise.all(importPromises))
})
})
function isWhitelistedFileName(fileName: string) {
// No whitelist means all files are whitelisted
if (!Array.isArray(whitelistedFileNames)) {
return true
}
return whitelistedFileNames.some((whitelistedFileName) => fileName === whitelistedFileName)
}
}