@tatara/cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bin.mjs +3001 -0
- package/package.json +32 -3
- package/templates/features/admin/files/src/apps/admin/app.ts +4 -0
- package/templates/features/admin/files/src/apps/admin/server.ts +5 -0
- package/templates/features/admin/files/src/modules/example/routes/admin-greeting.routes.ts +24 -0
- package/templates/features/admin/files/src/modules/example/tests/admin.test.ts +20 -0
- package/templates/features/auth/files/src/core/auth/middleware.ts +29 -0
- package/templates/features/auth/files/src/core/auth/routes/me.routes.ts +36 -0
- package/templates/features/auth/files/src/core/auth/tests/me.test.ts +25 -0
- package/templates/features/auth-orgs/files+drizzle+mysql/src/core/auth/db/organization.schema.ts +38 -0
- package/templates/features/auth-orgs/files+drizzle+postgres/src/core/auth/db/organization.schema.ts +38 -0
- package/templates/features/auth-orgs/files+prisma/prisma/models/organization.prisma +41 -0
- package/templates/features/authz/files/src/core/authz/cache.ts +31 -0
- package/templates/features/authz/files/src/core/authz/catalog.ts +85 -0
- package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +21 -0
- package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +11 -0
- package/templates/features/authz/files/src/core/authz/constants.ts +32 -0
- package/templates/features/authz/files/src/core/authz/db/seed.ts +10 -0
- package/templates/features/authz/files/src/core/authz/middleware.ts +21 -0
- package/templates/features/authz/files/src/core/authz/permissions.ts +31 -0
- package/templates/features/authz/files/src/core/authz/resolver.ts +98 -0
- package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +281 -0
- package/templates/features/authz/files/src/core/authz/services/audit.ts +24 -0
- package/templates/features/authz/files/src/core/authz/services/grants.ts +29 -0
- package/templates/features/authz/files/src/core/authz/services/members.ts +84 -0
- package/templates/features/authz/files/src/core/authz/services/roles.ts +73 -0
- package/templates/features/authz/files/src/core/authz/services/sync.ts +34 -0
- package/templates/features/authz/files/src/core/authz/system-roles.ts +9 -0
- package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +146 -0
- package/templates/features/authz/files/src/core/authz/tests/error-messages.test.ts +34 -0
- package/templates/features/authz/files/src/core/authz/tests/request-body.test.ts +27 -0
- package/templates/features/authz/files/src/core/authz/types.ts +87 -0
- package/templates/features/authz/files/src/core/authz/validators.ts +99 -0
- package/templates/features/authz/files/tests/helpers/authz.ts +30 -0
- package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +275 -0
- package/templates/features/authz/files+drizzle+mysql/src/core/authz/db/authz.schema.ts +90 -0
- package/templates/features/authz/files+drizzle+postgres/src/core/authz/db/authz.schema.ts +88 -0
- package/templates/features/authz/files+prisma/prisma/models/authz.prisma +75 -0
- package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +236 -0
- package/templates/features/base/files/AGENTS.md +71 -0
- package/templates/features/base/files/CLAUDE.md +1 -0
- package/templates/features/base/files/Dockerfile +16 -0
- package/templates/features/base/files/README.md +31 -0
- package/templates/features/base/files/_github/workflows/ci.yml +26 -0
- package/templates/features/base/files/biome.template.json +20 -0
- package/templates/features/base/files/dockerignore +8 -0
- package/templates/features/base/files/gitignore +9 -0
- package/templates/features/base/files/src/apps/api/app.ts +3 -0
- package/templates/features/base/files/src/apps/api/server.ts +5 -0
- package/templates/features/base/files/src/apps/cli/index.ts +6 -0
- package/templates/features/base/files/src/core/commands/define.ts +33 -0
- package/templates/features/base/files/src/core/commands/runner.ts +54 -0
- package/templates/features/base/files/src/core/commands/tests/flags.test.ts +16 -0
- package/templates/features/base/files/src/core/config/tests/env.test.ts +40 -0
- package/templates/features/base/files/src/core/http/app.ts +57 -0
- package/templates/features/base/files/src/core/http/context.ts +18 -0
- package/templates/features/base/files/src/core/http/errors.ts +79 -0
- package/templates/features/base/files/src/core/http/health.ts +25 -0
- package/templates/features/base/files/src/core/http/middleware/error-handler.ts +36 -0
- package/templates/features/base/files/src/core/http/middleware/request-context.ts +24 -0
- package/templates/features/base/files/src/core/http/middleware/validation.ts +17 -0
- package/templates/features/base/files/src/core/http/response.ts +72 -0
- package/templates/features/base/files/src/core/http/routes.ts +28 -0
- package/templates/features/base/files/src/core/http/server.ts +15 -0
- package/templates/features/base/files/src/core/logging/logger.ts +17 -0
- package/templates/features/base/files/src/core/process/lifecycle.ts +34 -0
- package/templates/features/base/files/src/modules/example/routes/greeting.routes.ts +27 -0
- package/templates/features/base/files/src/modules/example/services/greeting.service.ts +3 -0
- package/templates/features/base/files/src/modules/example/tests/greeting.test.ts +21 -0
- package/templates/features/base/files/src/modules/example/validators/greeting.validators.ts +9 -0
- package/templates/features/base/files/tests/helpers/api.ts +54 -0
- package/templates/features/base/files/tests/setup.ts +2 -0
- package/templates/features/base/files/tsconfig.json +20 -0
- package/templates/features/base/files/tsdown.config.ts +15 -0
- package/templates/features/base/files/vitest.config.ts +16 -0
- package/templates/features/better-auth/files/src/core/auth/auth.ts +26 -0
- package/templates/features/better-auth/files/src/core/auth/db/seed.ts +24 -0
- package/templates/features/better-auth/files/src/core/auth/delivery.ts +48 -0
- package/templates/features/better-auth/files/src/core/auth/openapi.ts +60 -0
- package/templates/features/better-auth/files/src/core/auth/principal.ts +40 -0
- package/templates/features/better-auth/files/src/core/auth/sms.ts +14 -0
- package/templates/features/better-auth/files/src/core/auth/tests/openapi.test.ts +45 -0
- package/templates/features/better-auth/files/tests/helpers/auth.ts +40 -0
- package/templates/features/better-auth/files+admin/src/core/auth/tests/openapi-admin.test.ts +8 -0
- package/templates/features/better-auth/files+auth-orgs/src/core/auth/tests/principal.test.ts +44 -0
- package/templates/features/better-auth/files+auth-orgs+authz/src/core/auth/tests/principal-authz.test.ts +40 -0
- package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/adapter.ts +15 -0
- package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/db/auth.schema.ts +57 -0
- package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/adapter.ts +15 -0
- package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/db/auth.schema.ts +56 -0
- package/templates/features/better-auth/files+prisma/prisma/models/auth.prisma +62 -0
- package/templates/features/better-auth/files+prisma+mysql/src/core/auth/adapter.ts +4 -0
- package/templates/features/better-auth/files+prisma+postgres/src/core/auth/adapter.ts +4 -0
- package/templates/features/db/files/src/core/db/commands/migrate.ts +12 -0
- package/templates/features/db/files/src/core/db/commands/reset.ts +19 -0
- package/templates/features/db/files/src/core/db/commands/seed.ts +13 -0
- package/templates/features/db/files/src/core/db/id.ts +13 -0
- package/templates/features/db/files/src/core/db/seed.ts +14 -0
- package/templates/features/db/files/src/core/db/tests/reset.test.ts +7 -0
- package/templates/features/db/files/tests/global-setup.ts +27 -0
- package/templates/features/db/files/tests/helpers/database-url.ts +9 -0
- package/templates/features/db/files/tests/helpers/db.ts +7 -0
- package/templates/features/db/files+mysql+drizzle/src/core/db/admin.ts +53 -0
- package/templates/features/db/files+mysql+prisma/src/core/db/admin.ts +78 -0
- package/templates/features/db/files+postgres/src/core/db/admin.ts +50 -0
- package/templates/features/drizzle/files+mysql/drizzle.config.ts +12 -0
- package/templates/features/drizzle/files+mysql/src/core/db/client.ts +20 -0
- package/templates/features/drizzle/files+mysql/src/core/db/migrate.ts +13 -0
- package/templates/features/drizzle/files+postgres/drizzle.config.ts +12 -0
- package/templates/features/drizzle/files+postgres/src/core/db/client.ts +20 -0
- package/templates/features/drizzle/files+postgres/src/core/db/migrate.ts +13 -0
- package/templates/features/email/files/src/core/email/commands/send-test.ts +13 -0
- package/templates/features/email/files/src/core/email/send.ts +22 -0
- package/templates/features/email/files/src/core/email/tests/email.test.ts +8 -0
- package/templates/features/i18n/files/src/core/i18n/index.ts +41 -0
- package/templates/features/i18n/files/src/core/i18n/tests/i18n.test.ts +21 -0
- package/templates/features/i18n/files/src/locales/ar.json +10 -0
- package/templates/features/i18n/files/src/locales/en.json +10 -0
- package/templates/features/jobs/files/src/apps/cron/index.ts +25 -0
- package/templates/features/jobs/files/src/apps/worker/index.ts +24 -0
- package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +13 -0
- package/templates/features/jobs/files/src/core/jobs/define.ts +37 -0
- package/templates/features/jobs/files/src/core/jobs/lock.ts +17 -0
- package/templates/features/jobs/files/src/core/jobs/process.ts +19 -0
- package/templates/features/jobs/files/src/core/jobs/queue.ts +29 -0
- package/templates/features/jobs/files/src/core/jobs/redis.ts +17 -0
- package/templates/features/jobs/files/src/modules/example/consumers/send-welcome.ts +11 -0
- package/templates/features/jobs/files/src/modules/example/schedules/heartbeat.ts +9 -0
- package/templates/features/jobs/files/src/modules/example/tests/jobs.test.ts +29 -0
- package/templates/features/keycloak/files/keycloak/realm.json +83 -0
- package/templates/features/keycloak/files/src/core/auth/commands/keycloak-token.ts +32 -0
- package/templates/features/keycloak/files/src/core/auth/keycloak.ts +58 -0
- package/templates/features/keycloak/files/src/core/auth/principal.ts +27 -0
- package/templates/features/keycloak/files/tests/helpers/auth.ts +52 -0
- package/templates/features/keycloak/files+drizzle/src/core/auth/users.ts +18 -0
- package/templates/features/keycloak/files+drizzle+mysql/src/core/auth/db/user.schema.ts +10 -0
- package/templates/features/keycloak/files+drizzle+postgres/src/core/auth/db/user.schema.ts +10 -0
- package/templates/features/keycloak/files+prisma/prisma/models/user.prisma +10 -0
- package/templates/features/keycloak/files+prisma/src/core/auth/users.ts +13 -0
- package/templates/features/prisma/files/prisma/models/.gitkeep +0 -0
- package/templates/features/prisma/files/prisma.config.ts +10 -0
- package/templates/features/prisma/files/src/core/db/migrate.ts +13 -0
- package/templates/features/prisma/files+mysql/prisma/schema.prisma +11 -0
- package/templates/features/prisma/files+mysql/src/core/db/client.ts +23 -0
- package/templates/features/prisma/files+postgres/prisma/schema.prisma +11 -0
- package/templates/features/prisma/files+postgres/src/core/db/client.ts +19 -0
- package/templates/features/storage/files/src/core/storage/commands/check.ts +18 -0
- package/templates/features/storage/files/src/core/storage/index.ts +8 -0
- package/templates/features/storage/files/src/core/storage/local.ts +34 -0
- package/templates/features/storage/files/src/core/storage/s3.ts +66 -0
- package/templates/features/storage/files/src/core/storage/storage.ts +21 -0
- package/templates/features/storage/files/src/core/storage/tests/storage.test.ts +22 -0
- package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts +11 -0
- package/templates/generators/consumer/files/src/modules/__moduleKebab__/consumers/__nameKebab__.ts +14 -0
- package/templates/generators/consumer/files/src/modules/__moduleKebab__/tests/__nameKebab__.consumer.test.ts +12 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/routes/__moduleKebab__.routes.ts +20 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/services/__moduleKebab__.service.ts +4 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/tests/__moduleKebab__.test.ts +10 -0
- package/templates/generators/module/files/src/modules/__moduleKebab__/validators/__moduleKebab__.validators.ts +5 -0
- package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +18 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +127 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/tests/__nameKebab__.routes.test.ts +59 -0
- package/templates/generators/resource/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +30 -0
- package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +21 -0
- package/templates/generators/resource/files+drizzle/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +78 -0
- package/templates/generators/resource/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/resource/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/resource/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
- package/templates/generators/resource/files+prisma/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +73 -0
- package/templates/generators/route/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +29 -0
- package/templates/generators/route/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +9 -0
- package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +13 -0
- package/templates/generators/schedule/files/src/modules/__moduleKebab__/schedules/__nameKebab__.ts +11 -0
- package/templates/generators/schema/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/schema/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
- package/templates/generators/schema/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
- package/templates/generators/service/files/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +4 -0
- package/templates/generators/service/files/src/modules/__moduleKebab__/tests/__nameKebab__.service.test.ts +8 -0
- package/templates/versions.json +34 -0
- package/README.md +0 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"lib": ["es2024"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"types": ["node"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "tests", "*.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
"apps/api/server": "src/apps/api/server.ts",
|
|
6
|
+
"apps/cli/index": "src/apps/cli/index.ts",
|
|
7
|
+
// tatara:slot entries
|
|
8
|
+
},
|
|
9
|
+
format: "esm",
|
|
10
|
+
platform: "node",
|
|
11
|
+
target: "node24",
|
|
12
|
+
outDir: "dist",
|
|
13
|
+
dts: false,
|
|
14
|
+
clean: true,
|
|
15
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { parseEnv } from "node:util";
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
4
|
+
|
|
5
|
+
const env = existsSync(".env.test")
|
|
6
|
+
? (parseEnv(readFileSync(".env.test", "utf8")) as Record<string, string>)
|
|
7
|
+
: {};
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
test: {
|
|
11
|
+
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
|
12
|
+
env,
|
|
13
|
+
setupFiles: ["tests/setup.ts"],
|
|
14
|
+
// tatara:slot vitest
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { bearer, openAPI } from "better-auth/plugins";
|
|
3
|
+
import { database } from "#core/auth/adapter";
|
|
4
|
+
import { env } from "#core/config/env";
|
|
5
|
+
import { newId } from "#core/db/id";
|
|
6
|
+
// tatara:slot imports
|
|
7
|
+
|
|
8
|
+
export const auth = betterAuth({
|
|
9
|
+
appName: "__APP_NAME__",
|
|
10
|
+
baseURL: env.APP_URL,
|
|
11
|
+
basePath: "/api/auth",
|
|
12
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
13
|
+
trustedOrigins: env.CORS_ORIGINS,
|
|
14
|
+
database,
|
|
15
|
+
advanced: { database: { generateId: () => newId() } },
|
|
16
|
+
// tatara:slot options
|
|
17
|
+
plugins: [
|
|
18
|
+
// Authorization: Bearer <session token> for mobile and API clients
|
|
19
|
+
bearer(),
|
|
20
|
+
// reference for the auth endpoints at /api/auth/reference
|
|
21
|
+
openAPI(),
|
|
22
|
+
// tatara:slot plugins
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type Session = typeof auth.$Infer.Session;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { auth } from "#core/auth/auth";
|
|
2
|
+
import { env } from "#core/config/env";
|
|
3
|
+
import { defineSeed } from "#core/db/seed";
|
|
4
|
+
|
|
5
|
+
/** Development admin account (never created in production). */
|
|
6
|
+
export default defineSeed({
|
|
7
|
+
name: "auth:dev-admin",
|
|
8
|
+
order: 0,
|
|
9
|
+
async run({ logger }) {
|
|
10
|
+
if (env.NODE_ENV !== "development" && env.NODE_ENV !== "test") return;
|
|
11
|
+
const ctx = await auth.$context;
|
|
12
|
+
const email = env.SEED_ADMIN_EMAIL;
|
|
13
|
+
const existing = await ctx.internalAdapter.findUserByEmail(email);
|
|
14
|
+
if (existing) return;
|
|
15
|
+
const user = await ctx.internalAdapter.createUser({
|
|
16
|
+
email,
|
|
17
|
+
name: "Dev admin",
|
|
18
|
+
emailVerified: true,
|
|
19
|
+
// tatara:slot seed-user-fields
|
|
20
|
+
}, { method: "admin" });
|
|
21
|
+
// tatara:slot seed-after-user
|
|
22
|
+
logger.info({ email, userId: user.id }, "created development admin");
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { smsSender } from "#core/auth/sms";
|
|
2
|
+
import { env } from "#core/config/env";
|
|
3
|
+
// tatara:slot imports
|
|
4
|
+
|
|
5
|
+
export interface OutboxMessage {
|
|
6
|
+
channel: "email" | "sms";
|
|
7
|
+
to: string;
|
|
8
|
+
subject?: string;
|
|
9
|
+
text: string;
|
|
10
|
+
/** one-time code, when the message carries one */
|
|
11
|
+
code?: string;
|
|
12
|
+
sentAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const outbox: OutboxMessage[] = [];
|
|
16
|
+
|
|
17
|
+
/** Only with DEV_OUTBOX=true in development or test: the outbox exposes sign-in codes. */
|
|
18
|
+
export function outboxEnabled(): boolean {
|
|
19
|
+
return env.DEV_OUTBOX && (env.NODE_ENV === "development" || env.NODE_ENV === "test");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Messages are kept here so tests and local sign-in can read codes. */
|
|
23
|
+
function record(message: Omit<OutboxMessage, "sentAt">) {
|
|
24
|
+
if (!outboxEnabled()) return;
|
|
25
|
+
outbox.push({ ...message, sentAt: new Date().toISOString() });
|
|
26
|
+
if (outbox.length > 200) outbox.shift();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Newest first. Empty unless the outbox is enabled. */
|
|
30
|
+
export function readOutbox(to?: string): OutboxMessage[] {
|
|
31
|
+
if (!outboxEnabled()) return [];
|
|
32
|
+
return outbox.filter((m) => !to || m.to === to).reverse();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function deliverEmail(message: {
|
|
36
|
+
to: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
text: string;
|
|
39
|
+
code?: string;
|
|
40
|
+
}): Promise<void> {
|
|
41
|
+
record({ channel: "email", ...message });
|
|
42
|
+
// tatara:slot email-delivery
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function deliverSms(message: { to: string; text: string; code?: string }): Promise<void> {
|
|
46
|
+
record({ channel: "sms", ...message });
|
|
47
|
+
await smsSender.send(message.to, message.text);
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { auth } from "#core/auth/auth";
|
|
3
|
+
|
|
4
|
+
type JsonObject = Record<string, unknown>;
|
|
5
|
+
type OpenApiDocument = ReturnType<OpenAPIHono["getOpenAPI31Document"]>;
|
|
6
|
+
type Paths = NonNullable<OpenApiDocument["paths"]>;
|
|
7
|
+
type Components = NonNullable<OpenApiDocument["components"]>;
|
|
8
|
+
|
|
9
|
+
interface AuthDocument {
|
|
10
|
+
paths?: Record<string, JsonObject>;
|
|
11
|
+
components?: { schemas?: JsonObject; securitySchemes?: JsonObject };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SCHEMA_REF = "#/components/schemas/";
|
|
15
|
+
|
|
16
|
+
// better-auth names schemas like the app (User, Member, …); prefix them so neither side is overwritten
|
|
17
|
+
function renameRefs(value: unknown, rename: (name: string) => string): AuthDocument {
|
|
18
|
+
return JSON.parse(
|
|
19
|
+
JSON.stringify(value).replaceAll(
|
|
20
|
+
new RegExp(`"\\$ref":"${SCHEMA_REF}([^"]+)"`, "g"),
|
|
21
|
+
(_match, name: string) => `"$ref":"${SCHEMA_REF}${rename(name)}"`,
|
|
22
|
+
),
|
|
23
|
+
) as AuthDocument;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function retag(paths: Record<string, JsonObject>, tag: string): Record<string, JsonObject> {
|
|
27
|
+
for (const operations of Object.values(paths)) {
|
|
28
|
+
for (const operation of Object.values(operations as Record<string, JsonObject>)) {
|
|
29
|
+
if (operation && typeof operation === "object") operation.tags = [tag];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Adds better-auth's endpoints (served under `basePath`) to the app's OpenAPI document. */
|
|
36
|
+
export async function mergeAuthOpenApi(document: OpenApiDocument, basePath: string): Promise<void> {
|
|
37
|
+
const authDoc = renameRefs(await auth.api.generateOpenAPISchema(), (name) => `Auth${name}`);
|
|
38
|
+
|
|
39
|
+
document.paths ??= {};
|
|
40
|
+
for (const [path, operations] of Object.entries(retag(authDoc.paths ?? {}, "Auth"))) {
|
|
41
|
+
document.paths[`${basePath}${path}`] = operations as Paths[string];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
document.components ??= {};
|
|
45
|
+
document.components.schemas ??= {};
|
|
46
|
+
for (const [name, schema] of Object.entries(authDoc.components?.schemas ?? {})) {
|
|
47
|
+
document.components.schemas[`Auth${name}`] = schema as NonNullable<
|
|
48
|
+
Components["schemas"]
|
|
49
|
+
>[string];
|
|
50
|
+
}
|
|
51
|
+
document.components.securitySchemes = {
|
|
52
|
+
...(authDoc.components?.securitySchemes as Components["securitySchemes"]),
|
|
53
|
+
...document.components.securitySchemes,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
document.tags = [
|
|
57
|
+
...(document.tags ?? []),
|
|
58
|
+
{ name: "Auth", description: "Sign-up, sign-in, sessions, organizations (better-auth)" },
|
|
59
|
+
];
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createMiddleware } from "hono/factory";
|
|
2
|
+
import { auth } from "#core/auth/auth";
|
|
3
|
+
import type { AppEnv } from "#core/http/context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The session's active organization, or null once the user stops being a member of it.
|
|
7
|
+
* better-auth leaves `activeOrganizationId` on the removed member's session.
|
|
8
|
+
*/
|
|
9
|
+
async function activeMembership(userId: string, orgId: string | null | undefined) {
|
|
10
|
+
if (!orgId) return null;
|
|
11
|
+
const { adapter } = await auth.$context;
|
|
12
|
+
const member = await adapter.findOne({
|
|
13
|
+
model: "member",
|
|
14
|
+
where: [
|
|
15
|
+
{ field: "organizationId", value: orgId },
|
|
16
|
+
{ field: "userId", value: userId },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
return member ? orgId : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolves the session (cookie or bearer token) into `c.var.principal`. */
|
|
23
|
+
export const attachPrincipal = createMiddleware<AppEnv>(async (c, next) => {
|
|
24
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
25
|
+
if (session) {
|
|
26
|
+
const user = session.user as typeof session.user & { role?: string | null };
|
|
27
|
+
const s = session.session as typeof session.session & { activeOrganizationId?: string | null };
|
|
28
|
+
c.set("principal", {
|
|
29
|
+
userId: user.id,
|
|
30
|
+
orgId: await activeMembership(user.id, s.activeOrganizationId),
|
|
31
|
+
roles: (user.role ?? "user")
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((role) => role.trim())
|
|
34
|
+
.filter(Boolean),
|
|
35
|
+
kind: "user",
|
|
36
|
+
claims: {},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
await next();
|
|
40
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { logger } from "#core/logging/logger";
|
|
2
|
+
|
|
3
|
+
/** Implement this for your SMS provider and return it from `smsSender`. */
|
|
4
|
+
export interface SmsSender {
|
|
5
|
+
send(to: string, text: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const consoleSender: SmsSender = {
|
|
9
|
+
async send(to) {
|
|
10
|
+
logger.info({ to }, "sms not sent: no SMS provider configured (see src/core/auth/sms.ts)");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const smsSender: SmsSender = consoleSender;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { api } from "#tests/helpers/api";
|
|
3
|
+
|
|
4
|
+
interface Document {
|
|
5
|
+
paths: Record<string, Record<string, { tags?: string[] }>>;
|
|
6
|
+
components: { schemas: Record<string, unknown>; securitySchemes: Record<string, unknown> };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function apiDocument(): Promise<Document> {
|
|
10
|
+
const res = await api.get<Document>("/openapi.json");
|
|
11
|
+
expect(res.status).toBe(200);
|
|
12
|
+
return res.body;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function refs(value: unknown): string[] {
|
|
16
|
+
return [...JSON.stringify(value).matchAll(/"\$ref":"#\/components\/schemas\/([^"]+)"/g)].map(
|
|
17
|
+
(match) => match[1] ?? "",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("OpenAPI document", () => {
|
|
22
|
+
it("documents better-auth endpoints next to the app's own", async () => {
|
|
23
|
+
const doc = await apiDocument();
|
|
24
|
+
expect(doc.paths["/api/auth/get-session"]?.get).toBeDefined();
|
|
25
|
+
expect(doc.paths["/api/me"]?.get).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("tags better-auth operations as Auth", async () => {
|
|
29
|
+
const doc = await apiDocument();
|
|
30
|
+
expect(doc.paths["/api/auth/get-session"]?.get?.tags).toEqual(["Auth"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("prefixes better-auth schemas so app schemas keep their names", async () => {
|
|
34
|
+
const doc = await apiDocument();
|
|
35
|
+
expect(doc.components.schemas.AuthUser).toBeDefined();
|
|
36
|
+
expect(doc.components.schemas.Principal).toBeDefined();
|
|
37
|
+
expect(doc.components.securitySchemes.bearerAuth).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("resolves every schema reference", async () => {
|
|
41
|
+
const doc = await apiDocument();
|
|
42
|
+
const missing = refs(doc).filter((name) => !(name in doc.components.schemas));
|
|
43
|
+
expect(missing).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { auth } from "#core/auth/auth";
|
|
2
|
+
import { newId } from "#core/db/id";
|
|
3
|
+
// tatara:slot imports
|
|
4
|
+
|
|
5
|
+
export interface AsUserOptions {
|
|
6
|
+
email?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
/** user roles (admin plugin); ignored without organizations + roles */
|
|
9
|
+
roles?: string[];
|
|
10
|
+
// tatara:slot options
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Creates a verified user and returns their id. */
|
|
14
|
+
export async function createTestUser(options: AsUserOptions = {}): Promise<string> {
|
|
15
|
+
const ctx = await auth.$context;
|
|
16
|
+
const user = await ctx.internalAdapter.createUser({
|
|
17
|
+
email: options.email ?? `user-${newId()}@example.com`,
|
|
18
|
+
name: options.name ?? "Test User",
|
|
19
|
+
emailVerified: true,
|
|
20
|
+
// tatara:slot user-fields
|
|
21
|
+
}, { method: "admin" });
|
|
22
|
+
return user.id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A new user with a real session; the token is sent as a bearer token. */
|
|
26
|
+
export async function authenticate(
|
|
27
|
+
options: AsUserOptions = {},
|
|
28
|
+
): Promise<{ userId: string; headers: Record<string, string> }> {
|
|
29
|
+
const userId = await createTestUser(options);
|
|
30
|
+
const ctx = await auth.$context;
|
|
31
|
+
const session = await ctx.internalAdapter.createSession(userId);
|
|
32
|
+
return { userId, headers: { authorization: `Bearer ${session.token}` } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Headers that authenticate as a new user. */
|
|
36
|
+
export async function asUser(options: AsUserOptions = {}): Promise<Record<string, string>> {
|
|
37
|
+
const user = await authenticate(options);
|
|
38
|
+
// tatara:slot as-user
|
|
39
|
+
return user.headers;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { adminApi } from "#tests/helpers/api";
|
|
3
|
+
|
|
4
|
+
it("leaves auth out of the admin OpenAPI document", async () => {
|
|
5
|
+
const res = await adminApi.get<{ paths: Record<string, unknown> }>("/openapi.json");
|
|
6
|
+
expect(res.status).toBe(200);
|
|
7
|
+
expect(Object.keys(res.body.paths).some((path) => path.startsWith("/api/auth/"))).toBe(false);
|
|
8
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { auth } from "#core/auth/auth";
|
|
3
|
+
import { api } from "#tests/helpers/api";
|
|
4
|
+
import { authenticate } from "#tests/helpers/auth";
|
|
5
|
+
import { resetDatabase } from "#tests/helpers/db";
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
await resetDatabase();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("principal", () => {
|
|
12
|
+
it("drops the active organization once the user is no longer a member", async () => {
|
|
13
|
+
const owner = await authenticate();
|
|
14
|
+
const member = await authenticate({ email: "member@example.com" });
|
|
15
|
+
const org = await api.post<{ id: string }>(
|
|
16
|
+
"/api/auth/organization/create",
|
|
17
|
+
{ name: "School", slug: "school" },
|
|
18
|
+
{ headers: owner.headers },
|
|
19
|
+
);
|
|
20
|
+
await auth.api.addMember({
|
|
21
|
+
body: { userId: member.userId, role: "member", organizationId: org.body.id },
|
|
22
|
+
});
|
|
23
|
+
await api.post(
|
|
24
|
+
"/api/auth/organization/set-active",
|
|
25
|
+
{ organizationId: org.body.id },
|
|
26
|
+
{ headers: member.headers },
|
|
27
|
+
);
|
|
28
|
+
const before = await api.get<{ data: { orgId: string | null } }>("/api/me", {
|
|
29
|
+
headers: member.headers,
|
|
30
|
+
});
|
|
31
|
+
expect(before.body.data.orgId).toBe(org.body.id);
|
|
32
|
+
|
|
33
|
+
const removed = await api.post(
|
|
34
|
+
"/api/auth/organization/remove-member",
|
|
35
|
+
{ memberIdOrEmail: "member@example.com", organizationId: org.body.id },
|
|
36
|
+
{ headers: owner.headers },
|
|
37
|
+
);
|
|
38
|
+
expect(removed.status).toBe(200);
|
|
39
|
+
const after = await api.get<{ data: { orgId: string | null } }>("/api/me", {
|
|
40
|
+
headers: member.headers,
|
|
41
|
+
});
|
|
42
|
+
expect(after.body.data.orgId).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { beforeEach, expect, it } from "vitest";
|
|
2
|
+
import { auth } from "#core/auth/auth";
|
|
3
|
+
import { api } from "#tests/helpers/api";
|
|
4
|
+
import { authenticate } from "#tests/helpers/auth";
|
|
5
|
+
import { applyTestAccess } from "#tests/helpers/authz";
|
|
6
|
+
import { resetDatabase } from "#tests/helpers/db";
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await resetDatabase();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("does not restore old org roles when a removed member is added back", async () => {
|
|
13
|
+
const owner = await authenticate();
|
|
14
|
+
const member = await authenticate({ email: "member@example.com" });
|
|
15
|
+
const org = await api.post<{ id: string }>(
|
|
16
|
+
"/api/auth/organization/create",
|
|
17
|
+
{ name: "School", slug: "school" },
|
|
18
|
+
{ headers: owner.headers },
|
|
19
|
+
);
|
|
20
|
+
await auth.api.addMember({
|
|
21
|
+
body: { userId: member.userId, role: "member", organizationId: org.body.id },
|
|
22
|
+
});
|
|
23
|
+
await applyTestAccess(member.userId, { systemRoles: ["admin"], orgId: org.body.id });
|
|
24
|
+
await api.post(
|
|
25
|
+
"/api/auth/organization/remove-member",
|
|
26
|
+
{ memberIdOrEmail: "member@example.com", organizationId: org.body.id },
|
|
27
|
+
{ headers: owner.headers },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await auth.api.addMember({
|
|
31
|
+
body: { userId: member.userId, role: "member", organizationId: org.body.id },
|
|
32
|
+
});
|
|
33
|
+
await api.post(
|
|
34
|
+
"/api/auth/organization/set-active",
|
|
35
|
+
{ organizationId: org.body.id },
|
|
36
|
+
{ headers: member.headers },
|
|
37
|
+
);
|
|
38
|
+
const roles = await api.get("/api/authorization/roles", { headers: member.headers });
|
|
39
|
+
expect(roles.status).toBe(403);
|
|
40
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
2
|
+
import * as authSchema from "#core/auth/db/auth.schema";
|
|
3
|
+
import { db } from "#core/db/client";
|
|
4
|
+
// tatara:slot imports
|
|
5
|
+
|
|
6
|
+
export const database = drizzleAdapter(db, {
|
|
7
|
+
provider: "mysql",
|
|
8
|
+
schema: {
|
|
9
|
+
user: authSchema.users,
|
|
10
|
+
session: authSchema.sessions,
|
|
11
|
+
account: authSchema.accounts,
|
|
12
|
+
verification: authSchema.verifications,
|
|
13
|
+
// tatara:slot adapter-schema
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { boolean, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
2
|
+
|
|
3
|
+
const id = () => varchar({ length: 36 });
|
|
4
|
+
const createdAt = () => timestamp({ fsp: 3, mode: "date" }).notNull().defaultNow();
|
|
5
|
+
const updatedAt = () => timestamp({ fsp: 3, mode: "date" }).notNull().defaultNow();
|
|
6
|
+
|
|
7
|
+
export const users = mysqlTable("users", {
|
|
8
|
+
id: id().primaryKey(),
|
|
9
|
+
name: varchar({ length: 255 }).notNull(),
|
|
10
|
+
email: varchar({ length: 255 }).notNull().unique(),
|
|
11
|
+
emailVerified: boolean().notNull().default(false),
|
|
12
|
+
image: text(),
|
|
13
|
+
// tatara:slot user-fields
|
|
14
|
+
createdAt: createdAt(),
|
|
15
|
+
updatedAt: updatedAt(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const sessions = mysqlTable("sessions", {
|
|
19
|
+
id: id().primaryKey(),
|
|
20
|
+
expiresAt: timestamp({ fsp: 3, mode: "date" }).notNull(),
|
|
21
|
+
token: varchar({ length: 255 }).notNull().unique(),
|
|
22
|
+
ipAddress: varchar({ length: 64 }),
|
|
23
|
+
userAgent: text(),
|
|
24
|
+
userId: id()
|
|
25
|
+
.notNull()
|
|
26
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
27
|
+
// tatara:slot session-fields
|
|
28
|
+
createdAt: createdAt(),
|
|
29
|
+
updatedAt: updatedAt(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const accounts = mysqlTable("accounts", {
|
|
33
|
+
id: id().primaryKey(),
|
|
34
|
+
accountId: varchar({ length: 255 }).notNull(),
|
|
35
|
+
providerId: varchar({ length: 64 }).notNull(),
|
|
36
|
+
userId: id()
|
|
37
|
+
.notNull()
|
|
38
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
39
|
+
accessToken: text(),
|
|
40
|
+
refreshToken: text(),
|
|
41
|
+
idToken: text(),
|
|
42
|
+
accessTokenExpiresAt: timestamp({ fsp: 3, mode: "date" }),
|
|
43
|
+
refreshTokenExpiresAt: timestamp({ fsp: 3, mode: "date" }),
|
|
44
|
+
scope: text(),
|
|
45
|
+
password: text(),
|
|
46
|
+
createdAt: createdAt(),
|
|
47
|
+
updatedAt: updatedAt(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const verifications = mysqlTable("verifications", {
|
|
51
|
+
id: id().primaryKey(),
|
|
52
|
+
identifier: varchar({ length: 255 }).notNull(),
|
|
53
|
+
value: text().notNull(),
|
|
54
|
+
expiresAt: timestamp({ fsp: 3, mode: "date" }).notNull(),
|
|
55
|
+
createdAt: createdAt(),
|
|
56
|
+
updatedAt: updatedAt(),
|
|
57
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
2
|
+
import * as authSchema from "#core/auth/db/auth.schema";
|
|
3
|
+
import { db } from "#core/db/client";
|
|
4
|
+
// tatara:slot imports
|
|
5
|
+
|
|
6
|
+
export const database = drizzleAdapter(db, {
|
|
7
|
+
provider: "pg",
|
|
8
|
+
schema: {
|
|
9
|
+
user: authSchema.users,
|
|
10
|
+
session: authSchema.sessions,
|
|
11
|
+
account: authSchema.accounts,
|
|
12
|
+
verification: authSchema.verifications,
|
|
13
|
+
// tatara:slot adapter-schema
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
const createdAt = () => timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow();
|
|
4
|
+
const updatedAt = () => timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow();
|
|
5
|
+
|
|
6
|
+
export const users = pgTable("users", {
|
|
7
|
+
id: text().primaryKey(),
|
|
8
|
+
name: text().notNull(),
|
|
9
|
+
email: text().notNull().unique(),
|
|
10
|
+
emailVerified: boolean().notNull().default(false),
|
|
11
|
+
image: text(),
|
|
12
|
+
// tatara:slot user-fields
|
|
13
|
+
createdAt: createdAt(),
|
|
14
|
+
updatedAt: updatedAt(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const sessions = pgTable("sessions", {
|
|
18
|
+
id: text().primaryKey(),
|
|
19
|
+
expiresAt: timestamp({ withTimezone: true, mode: "date" }).notNull(),
|
|
20
|
+
token: text().notNull().unique(),
|
|
21
|
+
ipAddress: text(),
|
|
22
|
+
userAgent: text(),
|
|
23
|
+
userId: text()
|
|
24
|
+
.notNull()
|
|
25
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
26
|
+
// tatara:slot session-fields
|
|
27
|
+
createdAt: createdAt(),
|
|
28
|
+
updatedAt: updatedAt(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const accounts = pgTable("accounts", {
|
|
32
|
+
id: text().primaryKey(),
|
|
33
|
+
accountId: text().notNull(),
|
|
34
|
+
providerId: text().notNull(),
|
|
35
|
+
userId: text()
|
|
36
|
+
.notNull()
|
|
37
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
38
|
+
accessToken: text(),
|
|
39
|
+
refreshToken: text(),
|
|
40
|
+
idToken: text(),
|
|
41
|
+
accessTokenExpiresAt: timestamp({ withTimezone: true, mode: "date" }),
|
|
42
|
+
refreshTokenExpiresAt: timestamp({ withTimezone: true, mode: "date" }),
|
|
43
|
+
scope: text(),
|
|
44
|
+
password: text(),
|
|
45
|
+
createdAt: createdAt(),
|
|
46
|
+
updatedAt: updatedAt(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const verifications = pgTable("verifications", {
|
|
50
|
+
id: text().primaryKey(),
|
|
51
|
+
identifier: text().notNull(),
|
|
52
|
+
value: text().notNull(),
|
|
53
|
+
expiresAt: timestamp({ withTimezone: true, mode: "date" }).notNull(),
|
|
54
|
+
createdAt: createdAt(),
|
|
55
|
+
updatedAt: updatedAt(),
|
|
56
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// better-auth tables. Field names are what better-auth expects; columns are snake_case.
|
|
2
|
+
|
|
3
|
+
model User {
|
|
4
|
+
id String @id @db.VarChar(36)
|
|
5
|
+
name String @db.VarChar(255)
|
|
6
|
+
email String @unique @db.VarChar(255)
|
|
7
|
+
emailVerified Boolean @default(false) @map("email_verified")
|
|
8
|
+
image String? @db.Text
|
|
9
|
+
// tatara:slot user-fields
|
|
10
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
11
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
12
|
+
sessions Session[]
|
|
13
|
+
accounts Account[]
|
|
14
|
+
// tatara:slot user-relations
|
|
15
|
+
|
|
16
|
+
@@map("users")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
model Session {
|
|
20
|
+
id String @id @db.VarChar(36)
|
|
21
|
+
expiresAt DateTime @map("expires_at")
|
|
22
|
+
token String @unique @db.VarChar(255)
|
|
23
|
+
ipAddress String? @map("ip_address") @db.VarChar(64)
|
|
24
|
+
userAgent String? @map("user_agent") @db.Text
|
|
25
|
+
userId String @map("user_id") @db.VarChar(36)
|
|
26
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
27
|
+
// tatara:slot session-fields
|
|
28
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
29
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
30
|
+
|
|
31
|
+
@@map("sessions")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
model Account {
|
|
35
|
+
id String @id @db.VarChar(36)
|
|
36
|
+
accountId String @map("account_id") @db.VarChar(255)
|
|
37
|
+
providerId String @map("provider_id") @db.VarChar(64)
|
|
38
|
+
userId String @map("user_id") @db.VarChar(36)
|
|
39
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
40
|
+
accessToken String? @map("access_token") @db.Text
|
|
41
|
+
refreshToken String? @map("refresh_token") @db.Text
|
|
42
|
+
idToken String? @map("id_token") @db.Text
|
|
43
|
+
accessTokenExpiresAt DateTime? @map("access_token_expires_at")
|
|
44
|
+
refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
|
|
45
|
+
scope String? @db.Text
|
|
46
|
+
password String? @db.Text
|
|
47
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
48
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
49
|
+
|
|
50
|
+
@@map("accounts")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
model Verification {
|
|
54
|
+
id String @id @db.VarChar(36)
|
|
55
|
+
identifier String @db.VarChar(255)
|
|
56
|
+
value String @db.Text
|
|
57
|
+
expiresAt DateTime @map("expires_at")
|
|
58
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
59
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
|
60
|
+
|
|
61
|
+
@@map("verifications")
|
|
62
|
+
}
|