Skip to content

Commit

Permalink
Create users & auth endpoints in backend (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
N2D4 committed Jul 2, 2024
1 parent fb825e6 commit 5345020
Show file tree
Hide file tree
Showing 248 changed files with 8,006 additions and 1,423 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ ui-debug.log
.husky
tmp

vitest.config.ts.timestamp-*

# Dependencies
node_modules

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,12 @@ pnpm run codegen

# Start the dev server
pnpm run dev

# In a different terminal, run tests in watch mode
pnpm run test
```

You can now open the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, and docs on port 8104. You can also run the tests with `pnpm run test:watch`.
You can now open the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106.

Your IDE may show an error on all `@stackframe/XYZ` imports. To fix this, simply restart the TypeScript language server; for example, in VSCode you can open the command palette (Ctrl+Shift+P) and run `Developer: Reload Window` or `TypeScript: Restart TS server`.

Expand Down
4 changes: 2 additions & 2 deletions apps/backend/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const withConfiguredSentryConfig = (nextConfig) =>

// Suppresses source map uploading logs during build
silent: true,
org: "stackframe-pw",
project: "stack-api",
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
},
{
// For all available options, see:
Expand Down
25 changes: 12 additions & 13 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@
"typecheck": "tsc --noEmit",
"with-env": "dotenv -c development --",
"with-env:prod": "dotenv -c --",
"dev": "concurrently \"next dev --port 8102\" \"npm run watch-docs\"",
"build": "npm run codegen && next build",
"analyze-bundle": "ANALYZE_BUNDLE=1 npm run build",
"dev": "concurrently \"next dev --port 8102\" \"pnpm run watch-docs\" \"pnpm run prisma-studio\"",
"build": "pnpm run codegen && next build",
"analyze-bundle": "ANALYZE_BUNDLE=1 pnpm run build",
"start": "next start --port 8102",
"codegen": "npm run prisma -- generate && npm run generate-docs",
"psql": "npm run with-env -- bash -c 'psql $STACK_DATABASE_CONNECTION_STRING'",
"prisma": "npm run with-env -- prisma",
"codegen": "pnpm run prisma generate && pnpm run generate-docs",
"psql": "pnpm run with-env bash -c 'psql $STACK_DATABASE_CONNECTION_STRING'",
"prisma": "pnpm run with-env prisma",
"prisma-studio": "pnpm run with-env prisma studio --port 8106 --browser none",
"lint": "next lint",
"watch-docs": "npm run with-env -- chokidar --silent '../../**/*' -i '../../docs/**' -i '../../**/node_modules/**' -i '../../**/.next/**' -i '../../**/dist/**' -c 'tsx scripts/generate-docs.ts'",
"generate-docs": "npm run with-env -- tsx scripts/generate-docs.ts",
"generate-keys": "npm run with-env -- tsx scripts/generate-keys.ts"
"watch-docs": "pnpm run with-env chokidar --silent '../../**/*' -i '../../docs/**' -i '../../**/node_modules/**' -i '../../**/.next/**' -i '../../**/dist/**' -c 'tsx scripts/generate-docs.ts'",
"generate-docs": "pnpm run with-env tsx scripts/generate-docs.ts",
"generate-keys": "pnpm run with-env tsx scripts/generate-keys.ts"
},
"prisma": {
"seed": "npm run with-env -- tsx prisma/seed.ts"
"seed": "pnpm run with-env tsx prisma/seed.ts"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@next/bundle-analyzer": "^14.0.3",
"@node-oauth/oauth2-server": "^5.1.0",
"@prisma/client": "^5.9.1",
Expand All @@ -32,13 +32,13 @@
"@react-email/tailwind": "^0.0.14",
"@sentry/nextjs": "^7.105.0",
"@stackframe/stack-shared": "workspace:*",
"@stackframe/stack-emails": "workspace:*",
"@vercel/analytics": "^1.2.2",
"bcrypt": "^5.1.1",
"date-fns": "^3.6.0",
"dotenv-cli": "^7.3.0",
"handlebars": "^4.7.8",
"jose": "^5.2.2",
"lodash": "^4.17.21",
"next": "^14.1",
"nodemailer": "^6.9.10",
"openid-client": "^5.6.4",
Expand All @@ -54,7 +54,6 @@
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/lodash": "^4.17.4",
"@types/node": "^20.8.10",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.66",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- CreateEnum
CREATE TYPE "VerificationCodeType" AS ENUM ('ONE_TIME_PASSWORD');

-- CreateTable
CREATE TABLE "VerificationCode" (
"projectId" TEXT NOT NULL,
"id" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"type" "VerificationCodeType" NOT NULL,
"code" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"usedAt" TIMESTAMP(3),
"redirectUrl" TEXT,
"email" TEXT NOT NULL,
"data" JSONB NOT NULL,

CONSTRAINT "VerificationCode_pkey" PRIMARY KEY ("projectId","id")
);

-- CreateIndex
CREATE UNIQUE INDEX "VerificationCode_projectId_code_key" ON "VerificationCode"("projectId", "code");
28 changes: 28 additions & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,32 @@ model ProjectUserAuthorizationCode {
@@id([projectId, authorizationCode])
}

model VerificationCode {
projectId String
id String @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type VerificationCodeType
code String
expiresAt DateTime
usedAt DateTime?
redirectUrl String?
email String
data Json
@@id([projectId, id])
@@unique([projectId, code])
}

enum VerificationCodeType {
ONE_TIME_PASSWORD
}

// @deprecated
model ProjectUserEmailVerificationCode {
projectId String
projectUserId String @db.Uuid
Expand All @@ -332,6 +358,7 @@ model ProjectUserEmailVerificationCode {
@@id([projectId, code])
}

// @deprecated
model ProjectUserPasswordResetCode {
projectId String
projectUserId String @db.Uuid
Expand All @@ -349,6 +376,7 @@ model ProjectUserPasswordResetCode {
@@id([projectId, code])
}

// @deprecated
model ProjectUserMagicLinkCode {
projectId String
projectUserId String @db.Uuid
Expand Down
1 change: 1 addition & 0 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async function seed() {
description: "Internal API key set",
publishableClientKey: "this-publishable-client-key-is-for-local-development-only",
secretServerKey: "this-secret-server-key-is-for-local-development-only",
superSecretAdminKey: "this-super-secret-admin-key-is-for-local-development-only",
expiresAt: new Date('2099-12-31T23:59:59Z'),
}],
},
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as Sentry from "@sentry/nextjs";

Sentry.init({
dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256",
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/sentry.edge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as Sentry from "@sentry/nextjs";

Sentry.init({
dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256",
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as Sentry from "@sentry/nextjs";

Sentry.init({
dsn: "https://0dc90570e0d280c1b4252ed61a328bfc@o4507084192022528.ingest.us.sentry.io/4507442898272256",
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/oauth/token/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
88 changes: 88 additions & 0 deletions apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as yup from "yup";
import { prismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { sendEmailFromTemplate } from "@/lib/emails";
import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { signInVerificationCodeHandler } from "../sign-in/verification-code-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, signInEmailSchema, verificationLinkRedirectUrlSchema } from "@stackframe/stack-shared/dist/schema-fields";
import { usersCrudHandlers } from "../../../users/crud";

export const POST = createSmartRouteHandler({
request: yup.object({
auth: yup.object({
type: clientOrHigherAuthTypeSchema,
project: adaptSchema,
}).required(),
body: yup.object({
email: signInEmailSchema.required(),
redirectUrl: verificationLinkRedirectUrlSchema,
}).required(),
}),
response: yup.object({
statusCode: yup.number().oneOf([200]).required(),
bodyType: yup.string().oneOf(["success"]).required(),
}),
async handler({ auth: { project }, body: { email, redirectUrl } }, fullReq) {
if (!project.evaluatedConfig.magicLinkEnabled) {
throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project");
}

const usersPrisma = await prismaClient.projectUser.findMany({
where: {
projectId: project.id,
primaryEmail: email,
authWithEmail: true,
},
});
if (usersPrisma.length > 1) {
throw new StackAssertionError(`Multiple users found in the database with the same primary email ${email}, and all with e-mail sign-in allowed. This should never happen (only non-email/OAuth accounts are allowed to share the same primaryEmail).`);
}

const userPrisma = usersPrisma.length > 0 ? usersPrisma[0] : null;
const isNewUser = !userPrisma;
let userObj: Pick<NonNullable<typeof userPrisma>, "projectUserId" | "displayName" | "primaryEmail"> | null = userPrisma;
if (!userObj) {
// TODO this should be in the same transaction as the read above
const createdUser = await usersCrudHandlers.adminCreate({
project,
data: {
auth_with_email: true,
primary_email: email,
primary_email_verified: false,
},
});
userObj = {
projectUserId: createdUser.id,
displayName: createdUser.display_name,
primaryEmail: createdUser.primary_email,
};
}

const { link } = await signInVerificationCodeHandler.sendCode({
project,
method: { email },
data: {
user_id: userObj.projectUserId,
is_new_user: isNewUser,
},
redirectUrl,
});

await sendEmailFromTemplate({
project,
email,
templateId: "MAGIC_LINK",
variables: {
userDisplayName: userObj.displayName,
userPrimaryEmail: userObj.primaryEmail,
projectDisplayName: project.displayName,
magicLink: link.toString(),
},
});

return {
statusCode: 200,
bodyType: "success",
};
},
});
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/otp/sign-in/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { signInVerificationCodeHandler } from "./verification-code-handler";

export const POST = signInVerificationCodeHandler.postHandler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as yup from "yup";
import { prismaClient } from "@/prisma-client";
import { createAuthTokens } from "@/lib/tokens";
import { createVerificationCodeHandler } from "@/route-handlers/verification-code-handler";
import { signInResponseSchema } from "@stackframe/stack-shared/dist/schema-fields";
import { VerificationCodeType } from "@prisma/client";

export const signInVerificationCodeHandler = createVerificationCodeHandler({
type: VerificationCodeType.ONE_TIME_PASSWORD,
data: yup.object({
user_id: yup.string().required(),
is_new_user: yup.boolean().required(),
}),
response: yup.object({
statusCode: yup.number().oneOf([200]).required(),
body: signInResponseSchema.required(),
}),
async handler(project, { email }, data) {
const projectUser = await prismaClient.projectUser.update({
where: {
projectId_projectUserId: {
projectId: project.id,
projectUserId: data.user_id,
},
primaryEmail: email,
},
data: {
primaryEmailVerified: true,
},
});

const { refreshToken, accessToken } = await createAuthTokens({
projectId: project.id,
projectUserId: projectUser.projectUserId,
});

return {
statusCode: 200,
body: {
refresh_token: refreshToken,
access_token: accessToken,
is_new_user: data.is_new_user,
user_id: data.user_id,
},
};
},
});
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/password/reset/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/password/sign-in/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/password/sign-up/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/password/update/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
3 changes: 3 additions & 0 deletions apps/backend/src/app/api/v1/auth/sign-out/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { NextResponse } from "next/server";

export const GET = () => NextResponse.json("TODO");
Loading

0 comments on commit 5345020

Please sign in to comment.