mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 19:14:52 +03:00
Remove generated files from git
This commit is contained in:
parent
487a01fd74
commit
1a09d71672
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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',
|
||||
})
|
@ -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',
|
||||
})
|
@ -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'
|
||||
}
|
@ -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
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
@ -1,2 +0,0 @@
|
||||
// todo(filip): turn into a proper import/path
|
||||
export type { AuthUser, ProviderName, DeserializedAuthIdentity } from 'wasp/server/_types'
|
@ -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
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
@ -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 })
|
||||
}
|
@ -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)
|
||||
}
|
@ -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}` }
|
||||
}
|
@ -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'
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user