@withmata/blueprints 0.2.0
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/.claude/commands/audit.md +179 -0
- package/.claude/commands/discover.md +92 -0
- package/.claude/commands/new-blueprint.md +265 -0
- package/.claude/commands/new-project.md +230 -0
- package/.claude/commands/scaffold-auth.md +310 -0
- package/.claude/commands/scaffold-db.md +270 -0
- package/.claude/commands/scaffold-foundation.md +158 -0
- package/.cursor/commands/audit.md +179 -0
- package/.cursor/commands/discover.md +92 -0
- package/.cursor/commands/new-blueprint.md +265 -0
- package/.cursor/commands/new-project.md +230 -0
- package/.cursor/commands/scaffold-auth.md +310 -0
- package/.cursor/commands/scaffold-db.md +270 -0
- package/.cursor/commands/scaffold-foundation.md +158 -0
- package/.opencode/commands/audit.md +183 -0
- package/.opencode/commands/discover.md +96 -0
- package/.opencode/commands/new-blueprint.md +269 -0
- package/.opencode/commands/new-project.md +234 -0
- package/.opencode/commands/scaffold-auth.md +314 -0
- package/.opencode/commands/scaffold-db.md +274 -0
- package/.opencode/commands/scaffold-foundation.md +162 -0
- package/ENGINEERING.md +47 -0
- package/blueprints/discovery/product-discovery/BLUEPRINT.md +361 -0
- package/blueprints/discovery/product-discovery/templates/archetype-template.md +143 -0
- package/blueprints/discovery/product-discovery/templates/product-brief.md +65 -0
- package/blueprints/discovery/product-discovery/templates/product-thesis.md +64 -0
- package/blueprints/discovery/product-discovery/templates/research-summary.md +43 -0
- package/blueprints/features/auth-better-auth/BLUEPRINT.md +794 -0
- package/blueprints/features/auth-better-auth/files/client/auth-client.ts +31 -0
- package/blueprints/features/auth-better-auth/files/client/context/organization.ts +236 -0
- package/blueprints/features/auth-better-auth/files/client/hooks/use-create-organization.ts +45 -0
- package/blueprints/features/auth-better-auth/files/client/hooks/use-has-permission.ts +26 -0
- package/blueprints/features/auth-better-auth/files/client/hooks/use-organization.ts +64 -0
- package/blueprints/features/auth-better-auth/files/client/schema/auth.ts +21 -0
- package/blueprints/features/auth-better-auth/files/client/schema/organization.ts +51 -0
- package/blueprints/features/auth-better-auth/files/client/types/organization.ts +20 -0
- package/blueprints/features/auth-better-auth/files/db/auth-schema.ts +184 -0
- package/blueprints/features/auth-better-auth/files/db/drizzle.config.ts +21 -0
- package/blueprints/features/auth-better-auth/files/middleware/route-protection.ts +84 -0
- package/blueprints/features/auth-better-auth/files/server/auth-db.ts +18 -0
- package/blueprints/features/auth-better-auth/files/server/auth.ts +159 -0
- package/blueprints/features/auth-better-auth/files/server/env.ts +44 -0
- package/blueprints/features/auth-better-auth/files/server/middleware.ts +144 -0
- package/blueprints/features/db-drizzle-postgres/BLUEPRINT.md +596 -0
- package/blueprints/features/db-drizzle-postgres/files/db/drizzle.config.ts +18 -0
- package/blueprints/features/db-drizzle-postgres/files/db/env.example +3 -0
- package/blueprints/features/db-drizzle-postgres/files/db/package.json +33 -0
- package/blueprints/features/db-drizzle-postgres/files/db/src/client.ts +34 -0
- package/blueprints/features/db-drizzle-postgres/files/db/src/example-entity.ts +73 -0
- package/blueprints/features/db-drizzle-postgres/files/db/src/index.ts +8 -0
- package/blueprints/features/db-drizzle-postgres/files/db/src/scripts/seed.ts +50 -0
- package/blueprints/features/db-drizzle-postgres/files/db/tsconfig.json +13 -0
- package/blueprints/features/tailwind-v4/BLUEPRINT.md +29 -0
- package/blueprints/features/ui-shared-components/BLUEPRINT.md +35 -0
- package/blueprints/foundation/monorepo-turbo/BLUEPRINT.md +378 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/app/globals.css +2 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/app/layout.tsx +23 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/app/page.tsx +7 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/env.ts +13 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/next.config.ts +12 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/package.json +28 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/postcss.config.mjs +5 -0
- package/blueprints/foundation/monorepo-turbo/files/apps/web/tsconfig.json +18 -0
- package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/package.json +14 -0
- package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/postcss.config.mjs +5 -0
- package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/shared-styles.css +88 -0
- package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/base.json +19 -0
- package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/nextjs.json +12 -0
- package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/package.json +5 -0
- package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/react-library.json +7 -0
- package/blueprints/foundation/monorepo-turbo/files/root/biome.json +46 -0
- package/blueprints/foundation/monorepo-turbo/files/root/gitignore +35 -0
- package/blueprints/foundation/monorepo-turbo/files/root/package.json +25 -0
- package/blueprints/foundation/monorepo-turbo/files/root/pnpm-workspace.yaml +5 -0
- package/blueprints/foundation/monorepo-turbo/files/root/turbo.json +30 -0
- package/dist/index.js +453 -0
- package/package.json +53 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Drizzle Kit Config — Auth Database
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Used by drizzle-kit to generate and run migrations for the auth database.
|
|
5
|
+
//
|
|
6
|
+
// Place this in your DB package: packages/db/drizzle/auth/drizzle.config.ts
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
import { config } from "dotenv";
|
|
10
|
+
import { defineConfig } from "drizzle-kit";
|
|
11
|
+
|
|
12
|
+
config({ path: "./.env" });
|
|
13
|
+
|
|
14
|
+
const AUTH_DATABASE_URL = process.env.AUTH_DATABASE_URL!;
|
|
15
|
+
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
schema: "./src/auth/schema.ts",
|
|
18
|
+
out: "./drizzle/auth",
|
|
19
|
+
dialect: "postgresql", // CONFIGURE: "postgresql" | "mysql" | "sqlite"
|
|
20
|
+
dbCredentials: { url: AUTH_DATABASE_URL },
|
|
21
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Next.js Middleware — Route Protection
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Protects routes by checking for the Better Auth session cookie.
|
|
5
|
+
// This is a COOKIE-PRESENCE CHECK only — actual session validation happens
|
|
6
|
+
// server-side in the auth middleware.
|
|
7
|
+
//
|
|
8
|
+
// Route classification:
|
|
9
|
+
// - Public: /auth/* — accessible without auth
|
|
10
|
+
// - Protected: everything else — redirects to /auth if no session cookie
|
|
11
|
+
// - Onboarding: /onboarding/* — accessible if authenticated
|
|
12
|
+
//
|
|
13
|
+
// Usage: Export this as middleware.ts in your Next.js app root, or integrate
|
|
14
|
+
// into your existing middleware.
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
import type { NextRequest } from "next/server";
|
|
18
|
+
import { NextResponse } from "next/server";
|
|
19
|
+
|
|
20
|
+
// CONFIGURE: adjust these route lists for your app
|
|
21
|
+
const publicRoutes = ["/auth", "/auth/"];
|
|
22
|
+
const publicPrefixes = ["/auth/"];
|
|
23
|
+
|
|
24
|
+
const onboardingRoutes = ["/onboarding", "/onboarding/"];
|
|
25
|
+
const onboardingPrefixes = ["/onboarding/"];
|
|
26
|
+
|
|
27
|
+
function isPublicRoute(pathname: string): boolean {
|
|
28
|
+
if (publicRoutes.includes(pathname)) return true;
|
|
29
|
+
return publicPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isOnboardingRoute(pathname: string): boolean {
|
|
33
|
+
if (onboardingRoutes.includes(pathname)) return true;
|
|
34
|
+
return onboardingPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function middleware(request: NextRequest) {
|
|
38
|
+
const { pathname } = request.nextUrl;
|
|
39
|
+
|
|
40
|
+
// Skip for static files and API routes
|
|
41
|
+
if (
|
|
42
|
+
pathname.startsWith("/_next") ||
|
|
43
|
+
pathname.startsWith("/api") ||
|
|
44
|
+
pathname.includes(".")
|
|
45
|
+
) {
|
|
46
|
+
return NextResponse.next();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for Better Auth session cookie
|
|
50
|
+
const sessionToken =
|
|
51
|
+
request.cookies.get("better-auth.session_token")?.value ||
|
|
52
|
+
request.cookies.get("__Secure-better-auth.session_token")?.value;
|
|
53
|
+
|
|
54
|
+
const isAuthenticated = !!sessionToken;
|
|
55
|
+
|
|
56
|
+
// Public routes — allow access
|
|
57
|
+
if (isPublicRoute(pathname)) {
|
|
58
|
+
// If authenticated and on /auth (exact), redirect to home
|
|
59
|
+
if (isAuthenticated && pathname === "/auth") {
|
|
60
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
61
|
+
}
|
|
62
|
+
return NextResponse.next();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Protected routes — require auth
|
|
66
|
+
if (!isAuthenticated) {
|
|
67
|
+
const signInUrl = new URL("/auth", request.url);
|
|
68
|
+
signInUrl.searchParams.set("returnUrl", pathname);
|
|
69
|
+
return NextResponse.redirect(signInUrl);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Onboarding routes — allow for authenticated users
|
|
73
|
+
if (isOnboardingRoute(pathname)) {
|
|
74
|
+
return NextResponse.next();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return NextResponse.next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const config = {
|
|
81
|
+
matcher: [
|
|
82
|
+
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
|
83
|
+
],
|
|
84
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Auth Database Connection
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Creates a Drizzle ORM instance connected to the auth-specific PostgreSQL
|
|
5
|
+
// database. This is separate from the app's core database.
|
|
6
|
+
//
|
|
7
|
+
// CONFIGURE: If using a different database driver (e.g. @neondatabase/serverless),
|
|
8
|
+
// swap the import and connection setup accordingly.
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
import * as schema from "{{SCOPE}}/db/auth"; // CONFIGURE: your DB package path
|
|
12
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
13
|
+
import pg from "pg";
|
|
14
|
+
import { env } from "./env.js";
|
|
15
|
+
|
|
16
|
+
const pool = new pg.Pool({ connectionString: env.AUTH_DATABASE_URL });
|
|
17
|
+
|
|
18
|
+
export const authDb = drizzle(pool, { schema });
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Better Auth Server Configuration
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// This is the core auth configuration. It is FRAMEWORK-AGNOSTIC — mount it on
|
|
5
|
+
// Hono, Next.js API routes, Express, Fastify, or any supported platform.
|
|
6
|
+
// See BLUEPRINT.md > Platform Integration for mounting instructions.
|
|
7
|
+
//
|
|
8
|
+
// Source: extracted from a production Turborepo monorepo.
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
import { betterAuth } from "better-auth";
|
|
12
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
13
|
+
import { openAPI } from "better-auth/plugins";
|
|
14
|
+
import { organization } from "better-auth/plugins/organization";
|
|
15
|
+
import { Resend } from "resend"; // CONFIGURE: swap email provider if needed
|
|
16
|
+
import { authDb } from "./auth-db.js";
|
|
17
|
+
import { env } from "./env.js";
|
|
18
|
+
|
|
19
|
+
// CONFIGURE: Replace with your email provider
|
|
20
|
+
const resend = new Resend(env.RESEND_API_KEY);
|
|
21
|
+
|
|
22
|
+
// Environment detection for local dev cookie fix
|
|
23
|
+
const isLocalDev = env.FRONTEND_DOMAIN.includes("localhost");
|
|
24
|
+
|
|
25
|
+
const auth = betterAuth({
|
|
26
|
+
// CORS: Which origins can make auth requests
|
|
27
|
+
trustedOrigins: [env.FRONTEND_DOMAIN],
|
|
28
|
+
|
|
29
|
+
emailAndPassword: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
requireEmailVerification: true,
|
|
32
|
+
sendResetPassword: async ({ user, token }) => {
|
|
33
|
+
const resetUrl = `${env.FRONTEND_DOMAIN}/auth/reset-password/${token}`;
|
|
34
|
+
try {
|
|
35
|
+
await resend.emails.send({
|
|
36
|
+
from: "{{APP_NAME}} <{{EMAIL_FROM}}>", // CONFIGURE: your app name and email
|
|
37
|
+
to: user.email,
|
|
38
|
+
subject: "Reset your password",
|
|
39
|
+
html: `
|
|
40
|
+
<div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 24px;">
|
|
41
|
+
<h2>Reset your password</h2>
|
|
42
|
+
<p>We received a request to reset the password for your {{APP_NAME}} account.</p>
|
|
43
|
+
<a href="${resetUrl}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 6px; margin-top: 16px;">
|
|
44
|
+
Reset Password
|
|
45
|
+
</a>
|
|
46
|
+
<p style="margin-top: 24px; font-size: 14px; color: #666;">
|
|
47
|
+
This link expires in 1 hour. If you didn't request this, you can safely ignore this email.
|
|
48
|
+
</p>
|
|
49
|
+
<p style="margin-top: 16px; font-size: 14px; color: #666;">
|
|
50
|
+
Or copy and paste this link into your browser:<br/>
|
|
51
|
+
<a href="${resetUrl}">${resetUrl}</a>
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Failed to send reset password email:", error);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// CONFIGURE: Add/remove social providers as needed
|
|
63
|
+
// See: https://www.better-auth.com/docs/authentication/social-sign-in
|
|
64
|
+
socialProviders: {
|
|
65
|
+
google: {
|
|
66
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
67
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Database adapter — uses Drizzle with PostgreSQL
|
|
72
|
+
database: drizzleAdapter(authDb, {
|
|
73
|
+
provider: "pg", // CONFIGURE: "pg" | "mysql" | "sqlite"
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
// Account linking: merge accounts when user signs in with different providers
|
|
77
|
+
account: {
|
|
78
|
+
accountLinking: {
|
|
79
|
+
enabled: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Session configuration
|
|
84
|
+
session: {
|
|
85
|
+
expiresIn: 60 * 60 * 24 * 30, // CONFIGURE: 30 days default
|
|
86
|
+
updateAge: 60 * 60 * 24, // Refresh session every 24 hours
|
|
87
|
+
cookieCache: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
maxAge: 60 * 5, // 5-minute cookie cache avoids DB round-trip per request
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// CONFIGURE: Add custom fields to the user model
|
|
94
|
+
user: {
|
|
95
|
+
additionalFields: {
|
|
96
|
+
currentOrganizationId: {
|
|
97
|
+
type: "string",
|
|
98
|
+
required: false,
|
|
99
|
+
input: false, // Server-managed, not settable via signup
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// CONFIGURE: Add/remove plugins
|
|
105
|
+
plugins: [
|
|
106
|
+
openAPI(), // Optional: exposes OpenAPI spec at /api/auth/reference
|
|
107
|
+
// CONFIGURE: Remove organization() if you don't need multi-tenancy
|
|
108
|
+
organization({
|
|
109
|
+
allowUserToCreateOrganization: true,
|
|
110
|
+
organizationLimit: 10, // CONFIGURE: max orgs per user
|
|
111
|
+
creatorRole: "owner",
|
|
112
|
+
membershipLimit: 100, // CONFIGURE: max members per org
|
|
113
|
+
disableOrganizationDeletion: true,
|
|
114
|
+
sendInvitationEmail: async (data) => {
|
|
115
|
+
const inviteUrl = `${env.FRONTEND_DOMAIN}/auth/invite/${data.id}?email=${encodeURIComponent(data.email)}`;
|
|
116
|
+
const inviterName = data.inviter.user.name;
|
|
117
|
+
const orgName = data.organization.name;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await resend.emails.send({
|
|
121
|
+
from: "{{APP_NAME}} <{{EMAIL_FROM}}>", // CONFIGURE: your app name and email
|
|
122
|
+
to: data.email,
|
|
123
|
+
subject: `${inviterName} invited you to join ${orgName}`,
|
|
124
|
+
html: `
|
|
125
|
+
<div style="font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 24px;">
|
|
126
|
+
<h2>You're invited!</h2>
|
|
127
|
+
<p><strong>${inviterName}</strong> has invited you to join <strong>${orgName}</strong> on {{APP_NAME}}.</p>
|
|
128
|
+
<a href="${inviteUrl}" style="display: inline-block; padding: 12px 24px; background-color: #000; color: #fff; text-decoration: none; border-radius: 6px; margin-top: 16px;">
|
|
129
|
+
Accept Invitation
|
|
130
|
+
</a>
|
|
131
|
+
<p style="margin-top: 24px; font-size: 14px; color: #666;">
|
|
132
|
+
Or copy and paste this link into your browser:<br/>
|
|
133
|
+
<a href="${inviteUrl}">${inviteUrl}</a>
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
`,
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Failed to send invitation email:", error);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
|
|
145
|
+
// Local dev: share cookies across localhost ports (e.g. frontend:3000, backend:4000)
|
|
146
|
+
// In production with proper domain setup, this isn't needed.
|
|
147
|
+
...(isLocalDev && {
|
|
148
|
+
advanced: {
|
|
149
|
+
crossSubDomainCookies: {
|
|
150
|
+
enabled: true,
|
|
151
|
+
domain: "localhost",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export type Auth = typeof auth;
|
|
158
|
+
|
|
159
|
+
export default auth;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Auth Environment Variables — Server-Side
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Validates auth-related environment variables at startup using t3-env + Zod.
|
|
5
|
+
//
|
|
6
|
+
// CONFIGURE: This is a RECOMMENDED pattern. If you prefer a different env
|
|
7
|
+
// validation approach, adapt accordingly. The key requirement is that all
|
|
8
|
+
// auth env vars are validated before the server starts.
|
|
9
|
+
//
|
|
10
|
+
// NOTE: This file only includes auth-related variables. Your project will
|
|
11
|
+
// likely have additional env vars — merge them into your project's env.ts.
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
import "dotenv/config";
|
|
15
|
+
import { createEnv } from "@t3-oss/env-core";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
export const env = createEnv({
|
|
19
|
+
server: {
|
|
20
|
+
// Server
|
|
21
|
+
PORT: z.preprocess(
|
|
22
|
+
(str) => parseInt(String(str), 10) || undefined,
|
|
23
|
+
z.number().min(1),
|
|
24
|
+
),
|
|
25
|
+
|
|
26
|
+
// Auth
|
|
27
|
+
FRONTEND_DOMAIN: z.url(), // Frontend URL for CORS, email links, trusted origins
|
|
28
|
+
BETTER_AUTH_URL: z.url(), // Base URL where auth API is accessible (OAuth callback target)
|
|
29
|
+
BETTER_AUTH_SECRET: z.string().min(1), // Generate with: openssl rand -base64 32
|
|
30
|
+
|
|
31
|
+
// Database
|
|
32
|
+
AUTH_DATABASE_URL: z.url(), // PostgreSQL connection URL for auth database
|
|
33
|
+
|
|
34
|
+
// Social providers — CONFIGURE: add/remove as needed
|
|
35
|
+
GOOGLE_CLIENT_ID: z.string().min(1),
|
|
36
|
+
GOOGLE_CLIENT_SECRET: z.string().min(1),
|
|
37
|
+
|
|
38
|
+
// Email — CONFIGURE: swap if using a different email provider
|
|
39
|
+
RESEND_API_KEY: z.string().min(1),
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
runtimeEnv: process.env,
|
|
43
|
+
emptyStringAsUndefined: true,
|
|
44
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Auth Middleware — Hono Reference Implementation
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// This file provides two middleware functions for protecting API routes:
|
|
5
|
+
// - requireAuth() — validates session, attaches user to context
|
|
6
|
+
// - requireOrganization() — validates session + active org membership
|
|
7
|
+
//
|
|
8
|
+
// FRAMEWORK NOTE: This is a Hono implementation. The core pattern is
|
|
9
|
+
// framework-agnostic — `auth.api.getSession({ headers })` works anywhere.
|
|
10
|
+
// Adapt the middleware wrapper for your framework. See BLUEPRINT.md > Platform
|
|
11
|
+
// Integration for examples with Next.js, Express, etc.
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
import type { Context, MiddlewareHandler, Next } from "hono";
|
|
15
|
+
import auth from "./auth.js";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export type SessionUser = {
|
|
22
|
+
id: string;
|
|
23
|
+
email: string;
|
|
24
|
+
name: string;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
image?: string | null;
|
|
27
|
+
currentOrganizationId?: string | null;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
updatedAt: Date;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type Session = {
|
|
33
|
+
id: string;
|
|
34
|
+
userId: string;
|
|
35
|
+
token: string;
|
|
36
|
+
expiresAt: Date;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
ipAddress?: string | null;
|
|
40
|
+
userAgent?: string | null;
|
|
41
|
+
activeOrganizationId?: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AuthContext = {
|
|
45
|
+
user: SessionUser;
|
|
46
|
+
session: Session;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Middleware
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates the session and attaches user info to context.
|
|
55
|
+
* Returns 401 if no valid session is found.
|
|
56
|
+
*/
|
|
57
|
+
export function requireAuth(): MiddlewareHandler<{
|
|
58
|
+
Variables: { auth: AuthContext };
|
|
59
|
+
}> {
|
|
60
|
+
return async (c: Context, next: Next) => {
|
|
61
|
+
try {
|
|
62
|
+
// Framework-agnostic core: auth.api.getSession({ headers })
|
|
63
|
+
const sessionData = await auth.api.getSession({
|
|
64
|
+
headers: c.req.raw.headers,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!sessionData || !sessionData.user) {
|
|
68
|
+
return c.json(
|
|
69
|
+
{ error: "Unauthorized", message: "Valid session required" },
|
|
70
|
+
401,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
c.set("auth", {
|
|
75
|
+
user: sessionData.user as SessionUser,
|
|
76
|
+
session: sessionData.session as Session,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await next();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Auth middleware error:", error);
|
|
82
|
+
return c.json(
|
|
83
|
+
{ error: "Unauthorized", message: "Session validation failed" },
|
|
84
|
+
401,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validates session AND checks for active organization.
|
|
92
|
+
* Returns 401 if no session, 403 if no active organization.
|
|
93
|
+
*
|
|
94
|
+
* CONFIGURE: Remove this if not using the organization plugin.
|
|
95
|
+
*/
|
|
96
|
+
export function requireOrganization(): MiddlewareHandler<{
|
|
97
|
+
Variables: { auth: AuthContext; organizationId: string };
|
|
98
|
+
}> {
|
|
99
|
+
return async (c: Context, next: Next) => {
|
|
100
|
+
try {
|
|
101
|
+
const sessionData = await auth.api.getSession({
|
|
102
|
+
headers: c.req.raw.headers,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!sessionData || !sessionData.user) {
|
|
106
|
+
return c.json(
|
|
107
|
+
{ error: "Unauthorized", message: "Valid session required" },
|
|
108
|
+
401,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const organizationId = sessionData.session.activeOrganizationId;
|
|
113
|
+
|
|
114
|
+
if (!organizationId) {
|
|
115
|
+
return c.json(
|
|
116
|
+
{ error: "Forbidden", message: "Active organization required" },
|
|
117
|
+
403,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
c.set("auth", {
|
|
122
|
+
user: sessionData.user as SessionUser,
|
|
123
|
+
session: sessionData.session as Session,
|
|
124
|
+
});
|
|
125
|
+
c.set("organizationId", organizationId);
|
|
126
|
+
|
|
127
|
+
await next();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error("Auth middleware error:", error);
|
|
130
|
+
return c.json(
|
|
131
|
+
{ error: "Unauthorized", message: "Session validation failed" },
|
|
132
|
+
401,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Helper to extract auth context from Hono context in route handlers.
|
|
140
|
+
* Assumes requireAuth() middleware has already run.
|
|
141
|
+
*/
|
|
142
|
+
export function getAuthContext(c: Context): AuthContext {
|
|
143
|
+
return c.get("auth") as AuthContext;
|
|
144
|
+
}
|