mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Compare commits
2 Commits
1650c55b19
...
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": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@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": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
"dev": "turbo run dev --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:web": "turbo run dev --filter=@documenso/web",
|
||||||
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
"dev:docs": "turbo run dev --filter=@documenso/documentation",
|
||||||
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
"dev:openpage-api": "turbo run dev --filter=@documenso/openpage-api",
|
||||||
@ -71,6 +72,7 @@
|
|||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
"next-runtime-env": "^3.2.0",
|
"next-runtime-env": "^3.2.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"typescript": "5.7.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
@ -80,4 +82,4 @@
|
|||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,6 +15,6 @@
|
|||||||
"eslint-plugin-package-json": "^0.10.4",
|
"eslint-plugin-package-json": "^0.10.4",
|
||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"eslint-plugin-unused-imports": "^3.1.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",
|
"dotenv-cli": "^7.3.0",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"tsx": "^4.11.0",
|
"tsx": "^4.11.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.7.2",
|
||||||
"zod-prisma-types": "^3.1.8"
|
"zod-prisma-types": "^3.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,10 +29,12 @@ enum Role {
|
|||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
secondaryId String @unique @default(cuid())
|
||||||
name String?
|
name String?
|
||||||
customerId String? @unique
|
customerId String? @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
|
isEmailVerified Boolean @default(false)
|
||||||
password String?
|
password String?
|
||||||
source String?
|
source String?
|
||||||
signature String?
|
signature String?
|
||||||
@ -44,18 +46,22 @@ model User {
|
|||||||
avatarImageId String?
|
avatarImageId String?
|
||||||
disabled Boolean @default(false)
|
disabled Boolean @default(false)
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
Document Document[]
|
Document Document[]
|
||||||
Subscription Subscription[]
|
Subscription Subscription[]
|
||||||
PasswordResetToken PasswordResetToken[]
|
PasswordResetToken PasswordResetToken[]
|
||||||
ownedTeams Team[]
|
ownedTeams Team[]
|
||||||
ownedPendingTeams TeamPending[]
|
ownedPendingTeams TeamPending[]
|
||||||
teamMembers TeamMember[]
|
teamMembers TeamMember[]
|
||||||
twoFactorSecret String?
|
twoFactorEnabled Boolean @default(false)
|
||||||
twoFactorEnabled Boolean @default(false)
|
|
||||||
|
// Todo: Delete these after full auth migration.
|
||||||
twoFactorBackupCodes String?
|
twoFactorBackupCodes String?
|
||||||
url String? @unique
|
twoFactorSecret String?
|
||||||
|
// End of Todo.
|
||||||
|
|
||||||
|
url String? @unique
|
||||||
|
|
||||||
profile UserProfile?
|
profile UserProfile?
|
||||||
VerificationToken VerificationToken[]
|
VerificationToken VerificationToken[]
|
||||||
@ -67,9 +73,21 @@ model User {
|
|||||||
passkeys Passkey[]
|
passkeys Passkey[]
|
||||||
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
avatarImage AvatarImage? @relation(fields: [avatarImageId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
image String?
|
||||||
|
|
||||||
|
twofactors TwoFactor[]
|
||||||
|
|
||||||
@@index([email])
|
@@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 {
|
model UserProfile {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(false)
|
enabled Boolean @default(false)
|
||||||
@ -248,7 +266,6 @@ model Subscription {
|
|||||||
model Account {
|
model Account {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId Int
|
userId Int
|
||||||
type String
|
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String
|
providerAccountId String
|
||||||
refresh_token String? @db.Text
|
refresh_token String? @db.Text
|
||||||
@ -256,12 +273,24 @@ model Account {
|
|||||||
expires_at Int?
|
expires_at Int?
|
||||||
// Some providers return created_at so we need to make it optional
|
// Some providers return created_at so we need to make it optional
|
||||||
created_at Int?
|
created_at Int?
|
||||||
// Stops next-auth from crashing when dealing with AzureAD
|
|
||||||
ext_expires_in Int?
|
|
||||||
token_type String?
|
|
||||||
scope String?
|
scope String?
|
||||||
id_token String? @db.Text
|
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)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@ -274,6 +303,23 @@ model Session {
|
|||||||
userId Int
|
userId Int
|
||||||
expires DateTime
|
expires DateTime
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
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 {
|
enum DocumentStatus {
|
||||||
|
|||||||
@ -18,6 +18,6 @@
|
|||||||
"ts-pattern": "^5.0.5"
|
"ts-pattern": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
|
|||||||
Reference in New Issue
Block a user