mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
Compare commits
2 Commits
88371b665a
...
wip/rr7-be
| Author | SHA1 | Date | |
|---|---|---|---|
| 071ce70292 | |||
| 866b036484 |
4
apps/remix/.dockerignore
Normal file
4
apps/remix/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
9
apps/remix/.gitignore
vendored
Normal file
9
apps/remix/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Vite
|
||||
vite.config.ts.timestamp*
|
||||
22
apps/remix/Dockerfile
Normal file
22
apps/remix/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM node:20-alpine AS development-dependencies-env
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS production-dependencies-env
|
||||
COPY ./package.json package-lock.json /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:20-alpine AS build-env
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
COPY ./package.json package-lock.json /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["npm", "run", "start"]
|
||||
25
apps/remix/Dockerfile.bun
Normal file
25
apps/remix/Dockerfile.bun
Normal file
@ -0,0 +1,25 @@
|
||||
FROM oven/bun:1 AS dependencies-env
|
||||
COPY . /app
|
||||
|
||||
FROM dependencies-env AS development-dependencies-env
|
||||
COPY ./package.json bun.lockb /app/
|
||||
WORKDIR /app
|
||||
RUN bun i --frozen-lockfile
|
||||
|
||||
FROM dependencies-env AS production-dependencies-env
|
||||
COPY ./package.json bun.lockb /app/
|
||||
WORKDIR /app
|
||||
RUN bun i --production
|
||||
|
||||
FROM dependencies-env AS build-env
|
||||
COPY ./package.json bun.lockb /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN bun run build
|
||||
|
||||
FROM dependencies-env
|
||||
COPY ./package.json bun.lockb /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["bun", "run", "start"]
|
||||
26
apps/remix/Dockerfile.pnpm
Normal file
26
apps/remix/Dockerfile.pnpm
Normal file
@ -0,0 +1,26 @@
|
||||
FROM node:20-alpine AS dependencies-env
|
||||
RUN npm i -g pnpm
|
||||
COPY . /app
|
||||
|
||||
FROM dependencies-env AS development-dependencies-env
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
WORKDIR /app
|
||||
RUN pnpm i --frozen-lockfile
|
||||
|
||||
FROM dependencies-env AS production-dependencies-env
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
WORKDIR /app
|
||||
RUN pnpm i --prod --frozen-lockfile
|
||||
|
||||
FROM dependencies-env AS build-env
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN pnpm build
|
||||
|
||||
FROM dependencies-env
|
||||
COPY ./package.json pnpm-lock.yaml /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["pnpm", "start"]
|
||||
100
apps/remix/README.md
Normal file
100
apps/remix/README.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Welcome to React Router!
|
||||
|
||||
A modern, production-ready template for building full-stack React applications using React Router.
|
||||
|
||||
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Server-side rendering
|
||||
- ⚡️ Hot Module Replacement (HMR)
|
||||
- 📦 Asset bundling and optimization
|
||||
- 🔄 Data loading and mutations
|
||||
- 🔒 TypeScript by default
|
||||
- 🎉 TailwindCSS for styling
|
||||
- 📖 [React Router docs](https://reactrouter.com/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Start the development server with HMR:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your application will be available at `http://localhost:5173`.
|
||||
|
||||
## Building for Production
|
||||
|
||||
Create a production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
This template includes three Dockerfiles optimized for different package managers:
|
||||
|
||||
- `Dockerfile` - for npm
|
||||
- `Dockerfile.pnpm` - for pnpm
|
||||
- `Dockerfile.bun` - for bun
|
||||
|
||||
To build and run using Docker:
|
||||
|
||||
```bash
|
||||
# For npm
|
||||
docker build -t my-app .
|
||||
|
||||
# For pnpm
|
||||
docker build -f Dockerfile.pnpm -t my-app .
|
||||
|
||||
# For bun
|
||||
docker build -f Dockerfile.bun -t my-app .
|
||||
|
||||
# Run the container
|
||||
docker run -p 3000:3000 my-app
|
||||
```
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### DIY Deployment
|
||||
|
||||
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||
|
||||
Make sure to deploy the output of `npm run build`
|
||||
|
||||
```
|
||||
├── package.json
|
||||
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||
├── build/
|
||||
│ ├── client/ # Static assets
|
||||
│ └── server/ # Server-side code
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ using React Router.
|
||||
1
apps/remix/app/app.css
Normal file
1
apps/remix/app/app.css
Normal file
@ -0,0 +1 @@
|
||||
@import '@documenso/ui/styles/theme.css';
|
||||
13
apps/remix/app/lib/auth-client.ts
Normal file
13
apps/remix/app/lib/auth-client.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { twoFactor } from 'better-auth/plugins';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
|
||||
import { passkeyClientPlugin } from './auth/passkey-plugin/client';
|
||||
|
||||
// make sure to import from better-auth/react
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: 'http://localhost:3000',
|
||||
plugins: [twoFactor(), passkeyClientPlugin()],
|
||||
});
|
||||
|
||||
export const { signIn, signOut, useSession } = authClient;
|
||||
112
apps/remix/app/lib/auth.server.ts
Normal file
112
apps/remix/app/lib/auth.server.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { compare, hash } from '@node-rs/bcrypt';
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||
import { twoFactor } from 'better-auth/plugins';
|
||||
|
||||
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { passkeyPlugin } from './auth/passkey-plugin';
|
||||
|
||||
// todo: import from @documenso/lib/constants/auth
|
||||
export const SALT_ROUNDS = 12;
|
||||
|
||||
const passkeyOptions = getAuthenticatorOptions();
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: 'Documenso',
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: 'Documenso',
|
||||
skipVerificationOnEnable: true,
|
||||
// totpOptions: {
|
||||
|
||||
// },
|
||||
schema: {
|
||||
twoFactor: {
|
||||
modelName: 'TwoFactor',
|
||||
fields: {
|
||||
userId: 'userId',
|
||||
secret: 'secret',
|
||||
backupCodes: 'backupCodes',
|
||||
},
|
||||
},
|
||||
},
|
||||
// todo: add options
|
||||
}),
|
||||
passkeyPlugin(),
|
||||
// passkey({
|
||||
// rpID: passkeyOptions.rpId,
|
||||
// rpName: passkeyOptions.rpName,
|
||||
// origin: passkeyOptions.origin,
|
||||
// schema: {
|
||||
// passkey: {
|
||||
// fields: {
|
||||
// publicKey: 'credentialPublicKey',
|
||||
// credentialID: 'credentialId',
|
||||
// deviceType: 'credentialDeviceType',
|
||||
// backedUp: 'credentialBackedUp',
|
||||
// // transports: '',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
],
|
||||
secret: 'secret', // todo
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: 'postgresql',
|
||||
}),
|
||||
databaseHooks: {
|
||||
account: {
|
||||
create: {
|
||||
before: (session) => {
|
||||
return {
|
||||
data: {
|
||||
...session,
|
||||
accountId: session.accountId.toString(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
fields: {
|
||||
token: 'sessionToken',
|
||||
expiresAt: 'expires',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
emailVerified: 'isEmailVerified',
|
||||
},
|
||||
},
|
||||
account: {
|
||||
fields: {
|
||||
providerId: 'provider',
|
||||
accountId: 'providerAccountId',
|
||||
refreshToken: 'refresh_token',
|
||||
accessToken: 'access_token',
|
||||
idToken: 'id_token',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
generateId: false,
|
||||
},
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
// maxPasswordLength: 128,
|
||||
// minPasswordLength: 8,
|
||||
password: {
|
||||
hash: async (password) => hash(password, SALT_ROUNDS),
|
||||
verify: async ({ hash, password }) => compare(password, hash),
|
||||
},
|
||||
},
|
||||
});
|
||||
24
apps/remix/app/lib/auth/passkey-plugin/client.ts
Normal file
24
apps/remix/app/lib/auth/passkey-plugin/client.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { BetterAuthClientPlugin } from 'better-auth';
|
||||
|
||||
import type { passkeyPlugin } from './index';
|
||||
|
||||
type PasskeyPlugin = typeof passkeyPlugin;
|
||||
|
||||
export const passkeyClientPlugin = () => {
|
||||
const passkeySignin = () => {
|
||||
//
|
||||
// credential: JSON.stringify(credential),
|
||||
// callbackUrl,
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'passkeyPlugin',
|
||||
getActions: () => ({
|
||||
signIn: {
|
||||
passkey: () => passkeySignin,
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
$InferServerPlugin: {} as ReturnType<PasskeyPlugin>,
|
||||
} satisfies BetterAuthClientPlugin;
|
||||
};
|
||||
165
apps/remix/app/lib/auth/passkey-plugin/index.ts
Normal file
165
apps/remix/app/lib/auth/passkey-plugin/index.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import type { BetterAuthPlugin } from 'better-auth';
|
||||
import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
|
||||
|
||||
export const passkeyPlugin = () =>
|
||||
({
|
||||
id: 'passkeyPlugin',
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
// twoFactorEnabled: {
|
||||
// type: 'boolean',
|
||||
// required: false,
|
||||
// },
|
||||
// twoFactorBackupCodes: {
|
||||
// type: 'string',
|
||||
// required: false,
|
||||
// },
|
||||
// twoFactorSecret: {
|
||||
// type: 'string',
|
||||
// required: false,
|
||||
// },
|
||||
// birthday: {
|
||||
// type: 'date', // string, number, boolean, date
|
||||
// required: true, // if the field should be required on a new record. (default: false)
|
||||
// unique: false, // if the field should be unique. (default: false)
|
||||
// reference: null, // if the field is a reference to another table. (default: null)
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
endpoints: {
|
||||
authorize: createAuthEndpoint(
|
||||
'/passkey/authorize',
|
||||
{
|
||||
method: 'POST',
|
||||
// use: [],
|
||||
},
|
||||
async (ctx) => {
|
||||
const csrfToken = credentials?.csrfToken;
|
||||
|
||||
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
|
||||
|
||||
try {
|
||||
const parsedBodyCredential = JSON.parse(req.body?.credential);
|
||||
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
|
||||
} catch {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const challengeToken = await prisma.anonymousVerificationToken
|
||||
.delete({
|
||||
where: {
|
||||
id: csrfToken,
|
||||
},
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!challengeToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (challengeToken.expiresAt < new Date()) {
|
||||
throw new AppError(AppErrorCode.EXPIRED_CODE);
|
||||
}
|
||||
|
||||
const passkey = await prisma.passkey.findFirst({
|
||||
where: {
|
||||
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
|
||||
},
|
||||
include: {
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!passkey) {
|
||||
throw new AppError(AppErrorCode.NOT_SETUP);
|
||||
}
|
||||
|
||||
const user = passkey.User;
|
||||
|
||||
const { rpId, origin } = getAuthenticatorOptions();
|
||||
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
response: requestBodyCrediential,
|
||||
expectedChallenge: challengeToken.token,
|
||||
expectedOrigin: origin,
|
||||
expectedRPID: rpId,
|
||||
authenticator: {
|
||||
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
|
||||
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
|
||||
counter: Number(passkey.counter),
|
||||
},
|
||||
}).catch(() => null);
|
||||
|
||||
const requestMetadata = extractNextAuthRequestMetadata(req);
|
||||
|
||||
if (!verification?.verified) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
await prisma.passkey.update({
|
||||
where: {
|
||||
id: passkey.id,
|
||||
},
|
||||
data: {
|
||||
lastUsedAt: new Date(),
|
||||
counter: verification.authenticationInfo.newCounter,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: Number(user.id),
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
emailVerified: user.emailVerified?.toISOString() ?? null,
|
||||
} satisfies User;
|
||||
},
|
||||
),
|
||||
},
|
||||
hooks: {
|
||||
before: [
|
||||
{
|
||||
matcher: (context) => context.path.startsWith('/sign-in/email'),
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
console.log('here...');
|
||||
|
||||
const { birthday } = ctx.body;
|
||||
|
||||
if ((!birthday) instanceof Date) {
|
||||
throw new APIError('BAD_REQUEST', { message: 'Birthday must be of type Date.' });
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5));
|
||||
|
||||
if (birthday >= fiveYearsAgo) {
|
||||
throw new APIError('BAD_REQUEST', { message: 'User must be above 5 years old.' });
|
||||
}
|
||||
|
||||
return { context: ctx };
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}) satisfies BetterAuthPlugin;
|
||||
74
apps/remix/app/root.tsx
Normal file
74
apps/remix/app/root.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
isRouteErrorResponse,
|
||||
} from 'react-router';
|
||||
|
||||
import type { Route } from './+types/root';
|
||||
import stylesheet from './app.css?url';
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.gstatic.com',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||
},
|
||||
{ rel: 'stylesheet', href: stylesheet },
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = 'Oops!';
|
||||
let details = 'An unexpected error occurred.';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? '404' : 'Error';
|
||||
details =
|
||||
error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container mx-auto p-4 pt-16">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full overflow-x-auto p-4">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
4
apps/remix/app/routes.ts
Normal file
4
apps/remix/app/routes.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { type RouteConfig } from '@react-router/dev/routes';
|
||||
import { flatRoutes } from '@react-router/fs-routes';
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
193
apps/remix/app/routes/_index.tsx
Normal file
193
apps/remix/app/routes/_index.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { authClient, signOut, useSession } from '~/lib/auth-client';
|
||||
import { auth } from '~/lib/auth.server';
|
||||
|
||||
import type { Route } from '../+types/root';
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: 'New React Router App' },
|
||||
{ name: 'description', content: 'Welcome to React Router!' },
|
||||
];
|
||||
}
|
||||
|
||||
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||
const session = await auth.api.getSession({
|
||||
query: {
|
||||
disableCookieCache: true,
|
||||
},
|
||||
headers: request.headers, // pass the headers
|
||||
});
|
||||
|
||||
return {
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
export function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
return {
|
||||
session: authClient.getSession(),
|
||||
};
|
||||
}
|
||||
|
||||
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
const { data } = useSession();
|
||||
|
||||
const [email, setEmail] = useState('deepfriedcoconut@gmail.com');
|
||||
const [password, setPassword] = useState('password');
|
||||
|
||||
const signIn = async () => {
|
||||
await authClient.signIn.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
// show loading state
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
console.log('success');
|
||||
// redirect to home
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log(ctx.error);
|
||||
alert(ctx.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const signUp = async () => {
|
||||
await authClient.signUp.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
// show loading state
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
console.log(ctx);
|
||||
// redirect to home
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log(ctx.error);
|
||||
alert(ctx.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center pb-4 pt-16">
|
||||
<h1>Status: {data ? 'Authenticated' : 'Not Authenticated'}</h1>
|
||||
|
||||
{data ? (
|
||||
<>
|
||||
<div>
|
||||
<p>Session data</p>
|
||||
<p className="mt-2 max-w-2xl text-xs">{JSON.stringify(data, null, 2)}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
authClient.twoFactor
|
||||
.enable({
|
||||
password: 'password', // user password required
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Enable 2FA
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
authClient.twoFactor.disable({
|
||||
password: 'password',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button onClick={() => signOut()}>signout</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="">
|
||||
<h2>Sign In</h2>
|
||||
<input
|
||||
className="border border-blue-500"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="border border-blue-500"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<button type="submit" onClick={signIn}>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2>Sign Up</h2>
|
||||
|
||||
<input
|
||||
type="email"
|
||||
className="border border-blue-500"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="border border-blue-500"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<button type="submit" onClick={signUp}>
|
||||
Sign Up
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
authClient.signIn.social({
|
||||
provider: 'google',
|
||||
});
|
||||
}}
|
||||
>
|
||||
google
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
const response = await authClient.signIn.passkey();
|
||||
console.log(response);
|
||||
}}
|
||||
>
|
||||
passkey
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
apps/remix/app/routes/api.auth.$.ts
Normal file
12
apps/remix/app/routes/api.auth.$.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// Adjust the path as necessary
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
|
||||
|
||||
import { auth } from '~/lib/auth.server';
|
||||
|
||||
export function loader({ request }: LoaderFunctionArgs) {
|
||||
return auth.handler(request);
|
||||
}
|
||||
|
||||
export function action({ request }: ActionFunctionArgs) {
|
||||
return auth.handler(request);
|
||||
}
|
||||
40
apps/remix/app/routes/signin.tsx
Normal file
40
apps/remix/app/routes/signin.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { authClient } from '~/lib/auth-client';
|
||||
|
||||
export default function SignIn() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const signIn = async () => {
|
||||
await authClient.signIn.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
{
|
||||
onRequest: (ctx) => {
|
||||
// show loading state
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// redirect to home
|
||||
},
|
||||
onError: (ctx) => {
|
||||
alert(ctx.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Sign In</h2>
|
||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
|
||||
<button type="submit" onClick={signIn}>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
apps/remix/package.json
Normal file
43
apps/remix/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@documenso/remix",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production react-router build",
|
||||
"dev": "tsx watch --ignore \"vite.config.ts*\" server/main.ts",
|
||||
"start": "cross-env NODE_ENV=production node dist/server/index.js",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
"@documenso/ee": "*",
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@epic-web/remember": "^1.1.0",
|
||||
"@hono/node-server": "^1.13.7",
|
||||
"@react-router/fs-routes": "^7.1.1",
|
||||
"@react-router/node": "^7.1.1",
|
||||
"@react-router/serve": "^7.1.1",
|
||||
"better-auth": "^1.1.9",
|
||||
"hono": "^4.6.15",
|
||||
"isbot": "^5.1.17",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-router": "^7.1.1",
|
||||
"remix-hono": "^0.0.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.1.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"cross-env": "^7.0.3",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript": "5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
BIN
apps/remix/public/favicon.ico
Normal file
BIN
apps/remix/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
apps/remix/react-router.config.ts
Normal file
7
apps/remix/react-router.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
|
||||
export default {
|
||||
appDirectory: 'app',
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
45
apps/remix/server/app.ts
Normal file
45
apps/remix/server/app.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { remember } from '@epic-web/remember';
|
||||
import { type HttpBindings } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { reactRouter } from 'remix-hono/handler';
|
||||
|
||||
type Bindings = HttpBindings;
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const viteDevServer = isProduction
|
||||
? undefined
|
||||
: await import('vite').then(async (vite) =>
|
||||
vite.createServer({
|
||||
server: { middlewareMode: true },
|
||||
}),
|
||||
);
|
||||
|
||||
const reactRouterMiddleware = remember('reactRouterMiddleware', async () =>
|
||||
reactRouter({
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
build: isProduction
|
||||
? // @ts-expect-error build/server/index.js is a build artifact
|
||||
await import('../build/server/index.js')
|
||||
: async () => viteDevServer!.ssrLoadModule('virtual:react-router/server-build'),
|
||||
}),
|
||||
);
|
||||
|
||||
// app.get('/', (c) => c.text('Hello, world!'));
|
||||
if (viteDevServer) {
|
||||
app.use('*', async (c, next) => {
|
||||
return new Promise((resolve) => {
|
||||
viteDevServer.middlewares(c.env.incoming, c.env.outgoing, () => resolve(next()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
const middleware = await reactRouterMiddleware;
|
||||
|
||||
return middleware(c, next);
|
||||
});
|
||||
|
||||
export default app;
|
||||
7
apps/remix/server/main.ts
Normal file
7
apps/remix/server/main.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
import app from './app';
|
||||
|
||||
serve(app, (info) => {
|
||||
console.log(`Server is running on http://localhost:${info.port}`);
|
||||
});
|
||||
18
apps/remix/tailwind.config.ts
Normal file
18
apps/remix/tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const baseConfig = require('@documenso/tailwind-config');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
content: [
|
||||
...baseConfig.content,
|
||||
'./app/**/*.{ts,tsx}',
|
||||
`${path.join(require.resolve('@documenso/ui'), '..')}/components/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/ui'), '..')}/icons/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/ui'), '..')}/lib/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/ui'), '..')}/primitives/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/email'), '..')}/templates/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/email'), '..')}/template-components/**/*.{ts,tsx}`,
|
||||
`${path.join(require.resolve('@documenso/email'), '..')}/providers/**/*.{ts,tsx}`,
|
||||
],
|
||||
};
|
||||
27
apps/remix/tsconfig.json
Normal file
27
apps/remix/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
17
apps/remix/vite.config.ts
Normal file
17
apps/remix/vite.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { reactRouter } from '@react-router/dev/vite';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [tailwindcss, autoprefixer],
|
||||
},
|
||||
},
|
||||
plugins: [reactRouter(), tsconfigPaths()],
|
||||
optimizeDeps: {
|
||||
exclude: ['@node-rs/bcrypt'],
|
||||
},
|
||||
});
|
||||
@ -73,6 +73,6 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
4319
package-lock.json
generated
4319
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
"dev": "turbo run dev --filter=@documenso/web",
|
||||
"dev:remix": "turbo run dev --filter=@documenso/remix",
|
||||
"dev:web": "turbo run dev --filter=@documenso/web",
|
||||
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||
@ -71,6 +72,7 @@
|
||||
"mupdf": "^1.0.0",
|
||||
"next-runtime-env": "^3.2.0",
|
||||
"react": "^18",
|
||||
"typescript": "5.7.2",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"overrides": {
|
||||
@ -80,4 +82,4 @@
|
||||
"trigger.dev": {
|
||||
"endpointId": "documenso-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,6 @@
|
||||
"eslint-plugin-package-json": "^0.10.4",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "secondaryId" TEXT;
|
||||
|
||||
-- Set all null secondaryId fields to a uuid
|
||||
UPDATE "User" SET "secondaryId" = gen_random_uuid()::text WHERE "secondaryId" IS NULL;
|
||||
|
||||
-- Restrict the User to required
|
||||
ALTER TABLE "User" ALTER COLUMN "secondaryId" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_secondaryId_key" ON "User"("secondaryId");
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `updatedAt` to the `Account` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "accessTokenExpiresAt" TIMESTAMP(3),
|
||||
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "password" TEXT,
|
||||
ADD COLUMN "refreshTokenExpiresAt" TIMESTAMP(3),
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
ALTER COLUMN "type" SET DEFAULT 'legacy';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Session" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "ipAddress" TEXT,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
ADD COLUMN "userAgent" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "image" TEXT,
|
||||
ADD COLUMN "isEmailVerified" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3),
|
||||
"updatedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@ -0,0 +1,28 @@
|
||||
-- Migrate DOCUMENSO users to have proper Account records
|
||||
DO $$
|
||||
BEGIN
|
||||
INSERT INTO "Account" (
|
||||
"id",
|
||||
"userId",
|
||||
"type",
|
||||
"provider",
|
||||
"providerAccountId",
|
||||
"password",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid()::text,
|
||||
u.id,
|
||||
'legacy',
|
||||
'credential',
|
||||
u.email,
|
||||
u.password,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM "User" u
|
||||
LEFT JOIN "Account" a ON a."userId" = u.id AND a."provider" = 'documenso'
|
||||
WHERE
|
||||
u."identityProvider" = 'DOCUMENSO'
|
||||
AND a.id IS NULL;
|
||||
END $$;
|
||||
@ -0,0 +1,32 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "TwoFactor" (
|
||||
"id" TEXT NOT NULL,
|
||||
"secret" TEXT NOT NULL,
|
||||
"backupCodes" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "TwoFactor_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TwoFactor" ADD CONSTRAINT "TwoFactor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Then migrate two factor data
|
||||
INSERT INTO "TwoFactor" (
|
||||
"secret",
|
||||
"backupCodes",
|
||||
"userId"
|
||||
)
|
||||
SELECT
|
||||
u."twoFactorSecret",
|
||||
COALESCE(u."twoFactorBackupCodes", ''),
|
||||
u.id
|
||||
FROM "User" u
|
||||
LEFT JOIN "TwoFactor" tf ON tf."userId" = u.id
|
||||
WHERE
|
||||
u."twoFactorSecret" IS NOT NULL
|
||||
AND u."twoFactorEnabled" = true
|
||||
AND tf.id IS NULL;
|
||||
END $$;
|
||||
@ -32,7 +32,7 @@
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.7.2",
|
||||
"zod-prisma-types": "^3.1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,10 +29,12 @@ enum Role {
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
secondaryId String @unique @default(cuid())
|
||||
name String?
|
||||
customerId String? @unique
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
isEmailVerified Boolean @default(false)
|
||||
password String?
|
||||
source String?
|
||||
signature String?
|
||||
@ -44,18 +46,22 @@ model User {
|
||||
avatarImageId String?
|
||||
disabled Boolean @default(false)
|
||||
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
Document Document[]
|
||||
Subscription Subscription[]
|
||||
PasswordResetToken PasswordResetToken[]
|
||||
ownedTeams Team[]
|
||||
ownedPendingTeams TeamPending[]
|
||||
teamMembers TeamMember[]
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
Document Document[]
|
||||
Subscription Subscription[]
|
||||
PasswordResetToken PasswordResetToken[]
|
||||
ownedTeams Team[]
|
||||
ownedPendingTeams TeamPending[]
|
||||
teamMembers TeamMember[]
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
|
||||
// Todo: Delete these after full auth migration.
|
||||
twoFactorBackupCodes String?
|
||||
url String? @unique
|
||||
twoFactorSecret String?
|
||||
// End of Todo.
|
||||
|
||||
url String? @unique
|
||||
|
||||
profile UserProfile?
|
||||
VerificationToken VerificationToken[]
|
||||
@ -67,9 +73,21 @@ model User {
|
||||
passkeys Passkey[]
|
||||
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
||||
|
||||
image String?
|
||||
|
||||
twofactors TwoFactor[]
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model TwoFactor {
|
||||
id String @id @default(cuid())
|
||||
secret String
|
||||
backupCodes String
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model UserProfile {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
@ -248,7 +266,6 @@ model Subscription {
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId Int
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
@ -256,12 +273,24 @@ model Account {
|
||||
expires_at Int?
|
||||
// Some providers return created_at so we need to make it optional
|
||||
created_at Int?
|
||||
// Stops next-auth from crashing when dealing with AzureAD
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
// Betterauth
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
password String?
|
||||
|
||||
// Stops next-auth from crashing when dealing with AzureAD
|
||||
ext_expires_in Int?
|
||||
|
||||
// Todo: Remove these fields after auth migration.
|
||||
type String @default("legacy")
|
||||
token_type String?
|
||||
session_state String?
|
||||
// End of Todo.
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@ -274,6 +303,23 @@ model Session {
|
||||
userId Int
|
||||
expires DateTime
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Better auth fields.
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id @default(cuid())
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime?
|
||||
updatedAt DateTime?
|
||||
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
enum DocumentStatus {
|
||||
|
||||
@ -18,6 +18,6 @@
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^1.3.1"
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"react": "^18",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
|
||||
Reference in New Issue
Block a user