fix: org project and apikey creating
This commit is contained in:
parent
bdf2c80782
commit
bfff9b49ec
|
|
@ -1,12 +1,11 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { ApiKeyService } from '../../services/ApiKeyService';
|
import { ApiKeyService, type ApiKeyWithSlugs } from '../../services/ApiKeyService';
|
||||||
import type { ApiKey } from '@banatie/database';
|
|
||||||
|
|
||||||
// Extend Express Request type to include apiKey
|
// Extend Express Request type to include apiKey with slugs
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
apiKey?: ApiKey;
|
apiKey?: ApiKeyWithSlugs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,17 @@ router.use(requireMasterKey);
|
||||||
*/
|
*/
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { type, projectId, organizationId, name, expiresInDays } = req.body;
|
const {
|
||||||
|
type,
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
organizationSlug,
|
||||||
|
projectSlug,
|
||||||
|
organizationName,
|
||||||
|
projectName,
|
||||||
|
name,
|
||||||
|
expiresInDays
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!type || !['master', 'project'].includes(type)) {
|
if (!type || !['master', 'project'].includes(type)) {
|
||||||
|
|
@ -26,23 +36,46 @@ router.post('/', async (req, res) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'project' && !projectId) {
|
if (type === 'project' && !projectSlug) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Missing projectId',
|
error: 'Missing projectSlug',
|
||||||
message: 'Project keys require a projectId',
|
message: 'Project keys require a projectSlug',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'project' && !organizationSlug) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing organizationSlug',
|
||||||
|
message: 'Project keys require an organizationSlug',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create key
|
// Create key
|
||||||
const result = type === 'master'
|
let result;
|
||||||
? await apiKeyService.createMasterKey(name, req.apiKey!.id)
|
|
||||||
: await apiKeyService.createProjectKey(
|
if (type === 'master') {
|
||||||
projectId,
|
result = await apiKeyService.createMasterKey(name, req.apiKey!.id);
|
||||||
organizationId,
|
} else {
|
||||||
|
// Get or create organization and project
|
||||||
|
const finalOrgId = await apiKeyService.getOrCreateOrganization(
|
||||||
|
organizationSlug,
|
||||||
|
organizationName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalProjectId = await apiKeyService.getOrCreateProject(
|
||||||
|
finalOrgId,
|
||||||
|
projectSlug,
|
||||||
|
projectName,
|
||||||
|
);
|
||||||
|
|
||||||
|
result = await apiKeyService.createProjectKey(
|
||||||
|
finalProjectId,
|
||||||
|
finalOrgId,
|
||||||
name,
|
name,
|
||||||
req.apiKey!.id,
|
req.apiKey!.id,
|
||||||
expiresInDays || 90
|
expiresInDays || 90
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] New API key created by admin: ${result.metadata.id} (${result.metadata.keyType}) - by: ${req.apiKey!.id}`);
|
console.log(`[${new Date().toISOString()}] New API key created by admin: ${result.metadata.id} (${result.metadata.keyType}) - by: ${req.apiKey!.id}`);
|
||||||
|
|
||||||
|
|
@ -52,6 +85,7 @@ router.post('/', async (req, res) => {
|
||||||
id: result.metadata.id,
|
id: result.metadata.id,
|
||||||
type: result.metadata.keyType,
|
type: result.metadata.keyType,
|
||||||
projectId: result.metadata.projectId,
|
projectId: result.metadata.projectId,
|
||||||
|
organizationId: result.metadata.organizationId,
|
||||||
name: result.metadata.name,
|
name: result.metadata.name,
|
||||||
expiresAt: result.metadata.expiresAt,
|
expiresAt: result.metadata.expiresAt,
|
||||||
scopes: result.metadata.scopes,
|
scopes: result.metadata.scopes,
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ generateRouter.post(
|
||||||
const { prompt, filename } = req.body;
|
const { prompt, filename } = req.body;
|
||||||
const files = (req.files as Express.Multer.File[]) || [];
|
const files = (req.files as Express.Multer.File[]) || [];
|
||||||
|
|
||||||
// Extract org/project IDs from validated API key
|
// Extract org/project slugs from validated API key
|
||||||
const orgId = req.apiKey?.organizationId || undefined;
|
const orgId = req.apiKey?.organizationSlug || undefined;
|
||||||
const projectId = req.apiKey?.projectId!; // Guaranteed by requireProjectKey middleware
|
const projectId = req.apiKey?.projectSlug!; // Guaranteed by requireProjectKey middleware
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${timestamp}] [${requestId}] Starting image generation process for org:${orgId}, project:${projectId}`,
|
`[${timestamp}] [${requestId}] Starting image generation process for org:${orgId}, project:${projectId}`,
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ textToImageRouter.post(
|
||||||
const requestId = req.requestId;
|
const requestId = req.requestId;
|
||||||
const { prompt, filename } = req.body;
|
const { prompt, filename } = req.body;
|
||||||
|
|
||||||
// Extract org/project IDs from validated API key
|
// Extract org/project slugs from validated API key
|
||||||
const orgId = req.apiKey?.organizationId || undefined;
|
const orgId = req.apiKey?.organizationSlug || undefined;
|
||||||
const projectId = req.apiKey?.projectId!; // Guaranteed by requireProjectKey middleware
|
const projectId = req.apiKey?.projectSlug!; // Guaranteed by requireProjectKey middleware
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${timestamp}] [${requestId}] Starting text-to-image generation process for org:${orgId}, project:${projectId}`,
|
`[${timestamp}] [${requestId}] Starting text-to-image generation process for org:${orgId}, project:${projectId}`,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { apiKeys, type ApiKey, type NewApiKey } from "@banatie/database";
|
import { apiKeys, organizations, projects, type ApiKey, type NewApiKey } from "@banatie/database";
|
||||||
import { eq, and, desc } from "drizzle-orm";
|
import { eq, and, desc } from "drizzle-orm";
|
||||||
|
|
||||||
|
// Extended API key type with slugs for storage paths
|
||||||
|
export interface ApiKeyWithSlugs extends ApiKey {
|
||||||
|
organizationSlug?: string;
|
||||||
|
projectSlug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiKeyService {
|
export class ApiKeyService {
|
||||||
/**
|
/**
|
||||||
* Generate a new API key
|
* Generate a new API key
|
||||||
|
|
@ -93,8 +99,9 @@ export class ApiKeyService {
|
||||||
/**
|
/**
|
||||||
* Validate an API key
|
* Validate an API key
|
||||||
* Returns null if invalid/expired/revoked
|
* Returns null if invalid/expired/revoked
|
||||||
|
* Returns API key with organization and project slugs for storage paths
|
||||||
*/
|
*/
|
||||||
async validateKey(providedKey: string): Promise<ApiKey | null> {
|
async validateKey(providedKey: string): Promise<ApiKeyWithSlugs | null> {
|
||||||
if (!providedKey || !providedKey.startsWith("bnt_")) {
|
if (!providedKey || !providedKey.startsWith("bnt_")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -105,14 +112,34 @@ export class ApiKeyService {
|
||||||
.update(providedKey)
|
.update(providedKey)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
// Find in database
|
// Find in database with left joins to get slugs
|
||||||
const [key] = await db
|
const [result] = await db
|
||||||
.select()
|
.select({
|
||||||
|
// API key fields
|
||||||
|
id: apiKeys.id,
|
||||||
|
keyHash: apiKeys.keyHash,
|
||||||
|
keyPrefix: apiKeys.keyPrefix,
|
||||||
|
keyType: apiKeys.keyType,
|
||||||
|
organizationId: apiKeys.organizationId,
|
||||||
|
projectId: apiKeys.projectId,
|
||||||
|
scopes: apiKeys.scopes,
|
||||||
|
createdAt: apiKeys.createdAt,
|
||||||
|
expiresAt: apiKeys.expiresAt,
|
||||||
|
lastUsedAt: apiKeys.lastUsedAt,
|
||||||
|
isActive: apiKeys.isActive,
|
||||||
|
name: apiKeys.name,
|
||||||
|
createdBy: apiKeys.createdBy,
|
||||||
|
// Slug fields
|
||||||
|
organizationSlug: organizations.slug,
|
||||||
|
projectSlug: projects.slug,
|
||||||
|
})
|
||||||
.from(apiKeys)
|
.from(apiKeys)
|
||||||
|
.leftJoin(organizations, eq(apiKeys.organizationId, organizations.id))
|
||||||
|
.leftJoin(projects, eq(apiKeys.projectId, projects.id))
|
||||||
.where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.isActive, true)))
|
.where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.isActive, true)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!key) {
|
if (!result) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[${new Date().toISOString()}] Invalid API key attempt: ${providedKey.substring(0, 10)}...`,
|
`[${new Date().toISOString()}] Invalid API key attempt: ${providedKey.substring(0, 10)}...`,
|
||||||
);
|
);
|
||||||
|
|
@ -120,9 +147,9 @@ export class ApiKeyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expiration
|
// Check expiration
|
||||||
if (key.expiresAt && key.expiresAt < new Date()) {
|
if (result.expiresAt && result.expiresAt < new Date()) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[${new Date().toISOString()}] Expired API key used: ${key.id} - expired: ${key.expiresAt.toISOString()}`,
|
`[${new Date().toISOString()}] Expired API key used: ${result.id} - expired: ${result.expiresAt.toISOString()}`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +157,7 @@ export class ApiKeyService {
|
||||||
// Update last used timestamp (async, don't wait)
|
// Update last used timestamp (async, don't wait)
|
||||||
db.update(apiKeys)
|
db.update(apiKeys)
|
||||||
.set({ lastUsedAt: new Date() })
|
.set({ lastUsedAt: new Date() })
|
||||||
.where(eq(apiKeys.id, key.id))
|
.where(eq(apiKeys.id, result.id))
|
||||||
.execute()
|
.execute()
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -139,7 +166,7 @@ export class ApiKeyService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return key;
|
return result as ApiKeyWithSlugs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,4 +202,83 @@ export class ApiKeyService {
|
||||||
|
|
||||||
return keys.length > 0;
|
return keys.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create organization by slug
|
||||||
|
* If organization doesn't exist, create it with provided name (or use slug as name)
|
||||||
|
*/
|
||||||
|
async getOrCreateOrganization(
|
||||||
|
slug: string,
|
||||||
|
name?: string,
|
||||||
|
email?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
// Try to find existing organization
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: organizations.id })
|
||||||
|
.from(organizations)
|
||||||
|
.where(eq(organizations.slug, slug))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new organization
|
||||||
|
const [newOrg] = await db
|
||||||
|
.insert(organizations)
|
||||||
|
.values({
|
||||||
|
slug,
|
||||||
|
name: name || slug,
|
||||||
|
email: email || `${slug}@placeholder.local`,
|
||||||
|
})
|
||||||
|
.returning({ id: organizations.id });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${new Date().toISOString()}] Organization created: ${newOrg?.id} - ${slug}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newOrg!.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create project by slug within an organization
|
||||||
|
* If project doesn't exist, create it with provided name (or use slug as name)
|
||||||
|
*/
|
||||||
|
async getOrCreateProject(
|
||||||
|
organizationId: string,
|
||||||
|
slug: string,
|
||||||
|
name?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
// Try to find existing project
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: projects.id })
|
||||||
|
.from(projects)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(projects.organizationId, organizationId),
|
||||||
|
eq(projects.slug, slug),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new project
|
||||||
|
const [newProject] = await db
|
||||||
|
.insert(projects)
|
||||||
|
.values({
|
||||||
|
organizationId,
|
||||||
|
slug,
|
||||||
|
name: name || slug,
|
||||||
|
})
|
||||||
|
.returning({ id: projects.id });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${new Date().toISOString()}] Project created: ${newProject?.id} - ${slug} (org: ${organizationId})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newProject!.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ const STORAGE_KEY = 'banatie_master_key';
|
||||||
export default function ApiKeysPage() {
|
export default function ApiKeysPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [masterKey, setMasterKey] = useState('');
|
const [masterKey, setMasterKey] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [orgSlug, setOrgSlug] = useState('');
|
||||||
const [orgName, setOrgName] = useState('');
|
const [projectSlug, setProjectSlug] = useState('');
|
||||||
const [projectName, setProjectName] = useState('');
|
|
||||||
const [generatedKey, setGeneratedKey] = useState('');
|
const [generatedKey, setGeneratedKey] = useState('');
|
||||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -45,15 +44,14 @@ export default function ApiKeysPage() {
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
setGeneratedKey('');
|
setGeneratedKey('');
|
||||||
|
|
||||||
const result = await createProjectApiKey(masterKey, email, orgName, projectName);
|
const result = await createProjectApiKey(masterKey, orgSlug, projectSlug);
|
||||||
|
|
||||||
if (result.success && result.apiKey) {
|
if (result.success && result.apiKey) {
|
||||||
setGeneratedKey(result.apiKey);
|
setGeneratedKey(result.apiKey);
|
||||||
setSuccess('API key created successfully!');
|
setSuccess('API key created successfully!');
|
||||||
// Clear form
|
// Clear form
|
||||||
setEmail('');
|
setOrgSlug('');
|
||||||
setOrgName('');
|
setProjectSlug('');
|
||||||
setProjectName('');
|
|
||||||
// Reload keys list
|
// Reload keys list
|
||||||
await loadApiKeys();
|
await loadApiKeys();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -121,25 +119,17 @@ export default function ApiKeysPage() {
|
||||||
<h2 className="text-2xl font-semibold text-white mb-6">Create New API Key</h2>
|
<h2 className="text-2xl font-semibold text-white mb-6">Create New API Key</h2>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<AdminFormInput
|
<AdminFormInput
|
||||||
label="Email"
|
label="Organization Slug"
|
||||||
type="email"
|
value={orgSlug}
|
||||||
value={email}
|
onChange={setOrgSlug}
|
||||||
onChange={setEmail}
|
placeholder="my-org"
|
||||||
placeholder="admin@example.com"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<AdminFormInput
|
<AdminFormInput
|
||||||
label="Organization Name"
|
label="Project Slug"
|
||||||
value={orgName}
|
value={projectSlug}
|
||||||
onChange={setOrgName}
|
onChange={setProjectSlug}
|
||||||
placeholder="My Organization"
|
placeholder="my-project"
|
||||||
required
|
|
||||||
/>
|
|
||||||
<AdminFormInput
|
|
||||||
label="Project Name"
|
|
||||||
value={projectName}
|
|
||||||
onChange={setProjectName}
|
|
||||||
placeholder="My Project"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<AdminButton type="submit" disabled={loading}>
|
<AdminButton type="submit" disabled={loading}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { getOrCreateOrgAndProject } from './orgProjectActions';
|
|
||||||
import { listApiKeys as listApiKeysQuery } from '../db/queries/apiKeys';
|
import { listApiKeys as listApiKeysQuery } from '../db/queries/apiKeys';
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||||
|
|
@ -51,15 +50,11 @@ export async function bootstrapMasterKey(): Promise<{ success: boolean; apiKey?:
|
||||||
|
|
||||||
export async function createProjectApiKey(
|
export async function createProjectApiKey(
|
||||||
masterKey: string,
|
masterKey: string,
|
||||||
email: string,
|
orgSlug: string,
|
||||||
orgName: string,
|
projectSlug: string
|
||||||
projectName: string
|
|
||||||
): Promise<{ success: boolean; apiKey?: string; error?: string }> {
|
): Promise<{ success: boolean; apiKey?: string; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// First, ensure organization and project exist in DB
|
// Call API service to create the project key (API auto-creates org/project)
|
||||||
const { organization, project } = await getOrCreateOrgAndProject(email, orgName, projectName);
|
|
||||||
|
|
||||||
// Then call API service to create the project key
|
|
||||||
const response = await fetch(`${API_BASE_URL}/api/admin/keys`, {
|
const response = await fetch(`${API_BASE_URL}/api/admin/keys`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -68,9 +63,9 @@ export async function createProjectApiKey(
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
type: 'project',
|
type: 'project',
|
||||||
projectId: project.id,
|
organizationSlug: orgSlug,
|
||||||
organizationId: organization.id,
|
projectSlug: projectSlug,
|
||||||
name: `${orgName} - ${projectName}`,
|
name: `${orgSlug} - ${projectSlug}`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "organizations" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "organizations_slug_unique" UNIQUE("slug"),
|
||||||
|
CONSTRAINT "organizations_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "projects" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"organization_id" uuid NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "projects_organization_id_slug_unique" UNIQUE("organization_id","slug")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "api_keys" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"key_hash" text NOT NULL,
|
||||||
|
"key_prefix" text DEFAULT 'bnt_' NOT NULL,
|
||||||
|
"key_type" text NOT NULL,
|
||||||
|
"organization_id" uuid,
|
||||||
|
"project_id" uuid,
|
||||||
|
"scopes" jsonb DEFAULT '["generate"]'::jsonb NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"expires_at" timestamp,
|
||||||
|
"last_used_at" timestamp,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"created_by" uuid,
|
||||||
|
CONSTRAINT "api_keys_key_hash_unique" UNIQUE("key_hash")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
{
|
||||||
|
"id": "8ec6e31f-1daa-4930-8bf8-2b4996e17270",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.organizations": {
|
||||||
|
"name": "organizations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"organizations_slug_unique": {
|
||||||
|
"name": "organizations_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"organizations_email_unique": {
|
||||||
|
"name": "organizations_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"name": "organization_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"projects_organization_id_organizations_id_fk": {
|
||||||
|
"name": "projects_organization_id_organizations_id_fk",
|
||||||
|
"tableFrom": "projects",
|
||||||
|
"tableTo": "organizations",
|
||||||
|
"columnsFrom": [
|
||||||
|
"organization_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"projects_organization_id_slug_unique": {
|
||||||
|
"name": "projects_organization_id_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"organization_id",
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.api_keys": {
|
||||||
|
"name": "api_keys",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"key_hash": {
|
||||||
|
"name": "key_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key_prefix": {
|
||||||
|
"name": "key_prefix",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'bnt_'"
|
||||||
|
},
|
||||||
|
"key_type": {
|
||||||
|
"name": "key_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"name": "organization_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scopes": {
|
||||||
|
"name": "scopes",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[\"generate\"]'::jsonb"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_used_at": {
|
||||||
|
"name": "last_used_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_by": {
|
||||||
|
"name": "created_by",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"api_keys_organization_id_organizations_id_fk": {
|
||||||
|
"name": "api_keys_organization_id_organizations_id_fk",
|
||||||
|
"tableFrom": "api_keys",
|
||||||
|
"tableTo": "organizations",
|
||||||
|
"columnsFrom": [
|
||||||
|
"organization_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"api_keys_project_id_projects_id_fk": {
|
||||||
|
"name": "api_keys_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "api_keys",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"api_keys_key_hash_unique": {
|
||||||
|
"name": "api_keys_key_hash_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"key_hash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"entries": []
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759661399219,
|
||||||
|
"tag": "0000_curious_wolfsbane",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ export const organizations = pgTable('organizations', {
|
||||||
|
|
||||||
// Organization details
|
// Organization details
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
|
slug: text('slug').notNull().unique(), // URL-friendly identifier for storage paths
|
||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,15 @@ export const projects = pgTable('projects', {
|
||||||
|
|
||||||
// Project details
|
// Project details
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
|
slug: text('slug').notNull(), // URL-friendly identifier for storage paths
|
||||||
organizationId: uuid('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),
|
organizationId: uuid('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => new Date()),
|
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => new Date()),
|
||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
// Unique constraint: one project name per organization
|
// Unique constraint: one project slug per organization
|
||||||
uniqueOrgProject: unique().on(table.organizationId, table.name),
|
uniqueOrgProjectSlug: unique().on(table.organizationId, table.slug),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type Project = typeof projects.$inferSelect;
|
export type Project = typeof projects.$inferSelect;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue