Compare commits
No commits in common. "f46a8d66d38c3dcedd5f60c4bae69c96b36687e0" and "f1335fb4d3e161ba5b96863959e58e148763a89f" have entirely different histories.
f46a8d66d3
...
f1335fb4d3
|
|
@ -112,25 +112,20 @@ router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const keys = await apiKeyService.listKeys();
|
const keys = await apiKeyService.listKeys();
|
||||||
|
|
||||||
// Format response with nested objects, ISO dates, and no sensitive data
|
// Don't expose key hashes
|
||||||
const safeKeys = keys.map((key) => ({
|
const safeKeys = keys.map((key) => ({
|
||||||
id: key.id,
|
id: key.id,
|
||||||
type: key.keyType,
|
type: key.keyType,
|
||||||
|
projectId: key.projectId,
|
||||||
name: key.name,
|
name: key.name,
|
||||||
scopes: key.scopes,
|
scopes: key.scopes,
|
||||||
isActive: key.isActive,
|
isActive: key.isActive,
|
||||||
createdAt: key.createdAt.toISOString(),
|
createdAt: key.createdAt,
|
||||||
expiresAt: key.expiresAt ? key.expiresAt.toISOString() : null,
|
expiresAt: key.expiresAt,
|
||||||
lastUsedAt: key.lastUsedAt ? key.lastUsedAt.toISOString() : null,
|
lastUsedAt: key.lastUsedAt,
|
||||||
createdBy: key.createdBy,
|
createdBy: key.createdBy,
|
||||||
organization: key.organization,
|
|
||||||
project: key.project,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[${new Date().toISOString()}] API keys listed by admin: ${req.apiKey!.id} - returned ${safeKeys.length} keys`,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
keys: safeKeys,
|
keys: safeKeys,
|
||||||
total: safeKeys.length,
|
total: safeKeys.length,
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,6 @@ export interface ApiKeyWithSlugs extends ApiKey {
|
||||||
projectSlug?: string;
|
projectSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended API key type with full organization and project details for admin listing
|
|
||||||
export interface ApiKeyWithDetails extends ApiKey {
|
|
||||||
organization: {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
} | null;
|
|
||||||
project: {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiKeyService {
|
export class ApiKeyService {
|
||||||
/**
|
/**
|
||||||
* Generate a new API key
|
* Generate a new API key
|
||||||
|
|
@ -197,73 +182,10 @@ export class ApiKeyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all keys (for admin) with organization and project details
|
* List all keys (for admin)
|
||||||
*/
|
*/
|
||||||
async listKeys(): Promise<ApiKeyWithDetails[]> {
|
async listKeys(): Promise<ApiKey[]> {
|
||||||
const results = await db
|
return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt));
|
||||||
.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,
|
|
||||||
// Organization fields
|
|
||||||
orgId: organizations.id,
|
|
||||||
orgSlug: organizations.slug,
|
|
||||||
orgName: organizations.name,
|
|
||||||
orgEmail: organizations.email,
|
|
||||||
// Project fields
|
|
||||||
projId: projects.id,
|
|
||||||
projSlug: projects.slug,
|
|
||||||
projName: projects.name,
|
|
||||||
})
|
|
||||||
.from(apiKeys)
|
|
||||||
.leftJoin(organizations, eq(apiKeys.organizationId, organizations.id))
|
|
||||||
.leftJoin(projects, eq(apiKeys.projectId, projects.id))
|
|
||||||
.orderBy(desc(apiKeys.createdAt));
|
|
||||||
|
|
||||||
// Transform flat results to nested structure
|
|
||||||
return results.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
keyHash: row.keyHash,
|
|
||||||
keyPrefix: row.keyPrefix,
|
|
||||||
keyType: row.keyType,
|
|
||||||
organizationId: row.organizationId,
|
|
||||||
projectId: row.projectId,
|
|
||||||
scopes: row.scopes,
|
|
||||||
createdAt: row.createdAt,
|
|
||||||
expiresAt: row.expiresAt,
|
|
||||||
lastUsedAt: row.lastUsedAt,
|
|
||||||
isActive: row.isActive,
|
|
||||||
name: row.name,
|
|
||||||
createdBy: row.createdBy,
|
|
||||||
organization:
|
|
||||||
row.orgId && row.orgSlug && row.orgName && row.orgEmail
|
|
||||||
? {
|
|
||||||
id: row.orgId,
|
|
||||||
slug: row.orgSlug,
|
|
||||||
name: row.orgName,
|
|
||||||
email: row.orgEmail,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
project:
|
|
||||||
row.projId && row.projSlug && row.projName
|
|
||||||
? {
|
|
||||||
id: row.projId,
|
|
||||||
slug: row.projSlug,
|
|
||||||
name: row.projName,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createProjectApiKey, listApiKeys } from '@/lib/actions/apiKeyActions';
|
||||||
import KeyDisplay from '@/components/admin/KeyDisplay';
|
import KeyDisplay from '@/components/admin/KeyDisplay';
|
||||||
import AdminFormInput from '@/components/admin/AdminFormInput';
|
import AdminFormInput from '@/components/admin/AdminFormInput';
|
||||||
import AdminButton from '@/components/admin/AdminButton';
|
import AdminButton from '@/components/admin/AdminButton';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
const STORAGE_KEY = 'banatie_master_key';
|
const STORAGE_KEY = 'banatie_master_key';
|
||||||
|
|
||||||
|
|
@ -27,16 +28,13 @@ export default function ApiKeysPage() {
|
||||||
router.push('/admin/master');
|
router.push('/admin/master');
|
||||||
} else {
|
} else {
|
||||||
setMasterKey(saved);
|
setMasterKey(saved);
|
||||||
// Load API keys with the saved master key
|
loadApiKeys();
|
||||||
listApiKeys(saved).then(setApiKeys);
|
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const loadApiKeys = async () => {
|
const loadApiKeys = async () => {
|
||||||
if (masterKey) {
|
const keys = await listApiKeys();
|
||||||
const keys = await listApiKeys(masterKey);
|
setApiKeys(keys);
|
||||||
setApiKeys(keys);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
|
@ -68,7 +66,23 @@ export default function ApiKeysPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-6 py-8">
|
<div className="relative z-10 max-w-6xl mx-auto px-6 py-16">
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mb-8 flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="/admin/master"
|
||||||
|
className="px-4 py-2 bg-slate-700 text-slate-300 rounded-lg font-medium hover:bg-slate-600"
|
||||||
|
>
|
||||||
|
Master Key
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/admin/apikeys"
|
||||||
|
className="px-4 py-2 bg-amber-600 text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
API Keys
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-4xl font-bold text-white mb-2">Project API Keys</h1>
|
<h1 className="text-4xl font-bold text-white mb-2">Project API Keys</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { SubsectionNav } from '@/components/shared/SubsectionNav';
|
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ label: 'Master Key', href: '/admin/master' },
|
|
||||||
{ label: 'API Keys', href: '/admin/apikeys' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AdminLayout({ children }: AdminLayoutProps) {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
|
||||||
{/* Animated gradient background */}
|
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
|
||||||
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
|
|
||||||
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/10 rounded-full blur-3xl animate-pulse delay-700"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subsection Navigation */}
|
|
||||||
<SubsectionNav items={navItems} currentPath={pathname} />
|
|
||||||
|
|
||||||
{/* Page Content */}
|
|
||||||
<div className="relative z-10">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { bootstrapMasterKey } from '@/lib/actions/apiKeyActions';
|
||||||
import KeyDisplay from '@/components/admin/KeyDisplay';
|
import KeyDisplay from '@/components/admin/KeyDisplay';
|
||||||
import AdminFormInput from '@/components/admin/AdminFormInput';
|
import AdminFormInput from '@/components/admin/AdminFormInput';
|
||||||
import AdminButton from '@/components/admin/AdminButton';
|
import AdminButton from '@/components/admin/AdminButton';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
const STORAGE_KEY = 'banatie_master_key';
|
const STORAGE_KEY = 'banatie_master_key';
|
||||||
|
|
||||||
|
|
@ -66,7 +67,23 @@ export default function MasterKeyPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto px-6 py-8">
|
<div className="relative z-10 max-w-4xl mx-auto px-6 py-16">
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mb-8 flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="/admin/master"
|
||||||
|
className="px-4 py-2 bg-amber-600 text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Master Key
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/admin/apikeys"
|
||||||
|
className="px-4 py-2 bg-slate-700 text-slate-300 rounded-lg font-medium hover:bg-slate-600"
|
||||||
|
>
|
||||||
|
API Keys
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-4xl font-bold text-white mb-2">Master Key Management</h1>
|
<h1 className="text-4xl font-bold text-white mb-2">Master Key Management</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
interface BootstrapResponse {
|
interface BootstrapResponse {
|
||||||
|
|
@ -24,48 +26,6 @@ interface CreateKeyResponse {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListKeysApiResponse {
|
|
||||||
keys: Array<{
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
name: string | null;
|
|
||||||
scopes: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
expiresAt: string | null;
|
|
||||||
lastUsedAt: string | null;
|
|
||||||
createdBy: string | null;
|
|
||||||
organization: {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
} | null;
|
|
||||||
project: {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
name: string;
|
|
||||||
} | null;
|
|
||||||
}>;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiKeyListItem {
|
|
||||||
id: string;
|
|
||||||
keyType: string;
|
|
||||||
name: string | null;
|
|
||||||
scopes: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
expiresAt: Date | null;
|
|
||||||
lastUsedAt: Date | null;
|
|
||||||
organizationId: string | null;
|
|
||||||
organizationName: string | null;
|
|
||||||
organizationEmail: string | null;
|
|
||||||
projectId: string | null;
|
|
||||||
projectName: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bootstrapMasterKey(): Promise<{
|
export async function bootstrapMasterKey(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
|
@ -126,45 +86,9 @@ export async function createProjectApiKey(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listApiKeys(masterKey: string): Promise<ApiKeyListItem[]> {
|
export async function listApiKeys() {
|
||||||
try {
|
try {
|
||||||
if (!masterKey) {
|
return await listApiKeysQuery();
|
||||||
console.error('Master key not provided');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/api/admin/keys`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-API-Key': masterKey,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Failed to fetch API keys:', response.statusText);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ListKeysApiResponse = await response.json();
|
|
||||||
|
|
||||||
// Transform nested API response to flat structure for backwards compatibility
|
|
||||||
return data.keys.map((key) => ({
|
|
||||||
id: key.id,
|
|
||||||
keyType: key.type,
|
|
||||||
name: key.name,
|
|
||||||
scopes: key.scopes,
|
|
||||||
isActive: key.isActive,
|
|
||||||
createdAt: new Date(key.createdAt),
|
|
||||||
expiresAt: key.expiresAt ? new Date(key.expiresAt) : null,
|
|
||||||
lastUsedAt: key.lastUsedAt ? new Date(key.lastUsedAt) : null,
|
|
||||||
organizationId: key.organization?.id || null,
|
|
||||||
organizationName: key.organization?.name || null,
|
|
||||||
organizationEmail: key.organization?.email || null,
|
|
||||||
projectId: key.project?.id || null,
|
|
||||||
projectName: key.project?.name || null,
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('List keys error:', error);
|
console.error('List keys error:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getOrganizationByEmail, createOrganization } from '../db/queries/organizations';
|
||||||
|
import { getProjectByName, createProject } from '../db/queries/projects';
|
||||||
|
import type { Organization, Project } from '@banatie/database';
|
||||||
|
|
||||||
|
export async function getOrCreateOrganization(email: string, name: string): Promise<Organization> {
|
||||||
|
// Try to find existing organization
|
||||||
|
const existing = await getOrganizationByEmail(email);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new organization
|
||||||
|
return createOrganization({ email, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateProject(organizationId: string, name: string): Promise<Project> {
|
||||||
|
// Try to find existing project
|
||||||
|
const existing = await getProjectByName(organizationId, name);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new project
|
||||||
|
return createProject({ organizationId, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateOrgAndProject(
|
||||||
|
email: string,
|
||||||
|
orgName: string,
|
||||||
|
projectName: string,
|
||||||
|
): Promise<{ organization: Organization; project: Project }> {
|
||||||
|
// Get or create organization
|
||||||
|
const organization = await getOrCreateOrganization(email, orgName);
|
||||||
|
|
||||||
|
// Get or create project
|
||||||
|
const project = await getOrCreateProject(organization.id, projectName);
|
||||||
|
|
||||||
|
return { organization, project };
|
||||||
|
}
|
||||||
|
|
@ -63,8 +63,8 @@ export interface ApiKeyContextValue {
|
||||||
|
|
||||||
// Focus method for external components
|
// Focus method for external components
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
inputRef: React.Ref<HTMLInputElement>;
|
||||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
containerRef: React.Ref<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
import * as schema from '@banatie/database';
|
||||||
|
|
||||||
|
const connectionString =
|
||||||
|
process.env.DATABASE_URL ||
|
||||||
|
'postgresql://banatie_user:banatie_secure_password@localhost:5460/banatie_db';
|
||||||
|
|
||||||
|
// Create postgres client
|
||||||
|
const client = postgres(connectionString);
|
||||||
|
|
||||||
|
// Create drizzle instance with schema
|
||||||
|
export const db = drizzle(client, { schema });
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { db } from '../client';
|
||||||
|
import { apiKeys, organizations, projects, type ApiKey } from '@banatie/database';
|
||||||
|
import { eq, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function listApiKeys() {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: apiKeys.id,
|
||||||
|
keyType: apiKeys.keyType,
|
||||||
|
name: apiKeys.name,
|
||||||
|
scopes: apiKeys.scopes,
|
||||||
|
isActive: apiKeys.isActive,
|
||||||
|
createdAt: apiKeys.createdAt,
|
||||||
|
expiresAt: apiKeys.expiresAt,
|
||||||
|
lastUsedAt: apiKeys.lastUsedAt,
|
||||||
|
organizationId: apiKeys.organizationId,
|
||||||
|
organizationName: organizations.name,
|
||||||
|
organizationEmail: organizations.email,
|
||||||
|
projectId: apiKeys.projectId,
|
||||||
|
projectName: projects.name,
|
||||||
|
})
|
||||||
|
.from(apiKeys)
|
||||||
|
.leftJoin(organizations, eq(apiKeys.organizationId, organizations.id))
|
||||||
|
.leftJoin(projects, eq(apiKeys.projectId, projects.id))
|
||||||
|
.orderBy(desc(apiKeys.createdAt))
|
||||||
|
.limit(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApiKeysByProject(projectId: string) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(apiKeys)
|
||||||
|
.where(eq(apiKeys.projectId, projectId))
|
||||||
|
.orderBy(desc(apiKeys.createdAt));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { db } from '../client';
|
||||||
|
import { organizations, type Organization, type NewOrganization } from '@banatie/database';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function getOrganizationByEmail(email: string): Promise<Organization | null> {
|
||||||
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(organizations)
|
||||||
|
.where(eq(organizations.email, email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return org || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrganization(data: NewOrganization): Promise<Organization> {
|
||||||
|
const [org] = await db.insert(organizations).values(data).returning();
|
||||||
|
|
||||||
|
return org!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrganizations(): Promise<Organization[]> {
|
||||||
|
return db.select().from(organizations).orderBy(organizations.createdAt);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { db } from '../client';
|
||||||
|
import { projects, type Project, type NewProject } from '@banatie/database';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function getProjectByName(
|
||||||
|
organizationId: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<Project | null> {
|
||||||
|
const [project] = await db
|
||||||
|
.select()
|
||||||
|
.from(projects)
|
||||||
|
.where(and(eq(projects.organizationId, organizationId), eq(projects.name, name)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return project || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProject(data: NewProject): Promise<Project> {
|
||||||
|
const [project] = await db.insert(projects).values(data).returning();
|
||||||
|
|
||||||
|
return project!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listProjectsByOrganization(organizationId: string): Promise<Project[]> {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.organizationId, organizationId))
|
||||||
|
.orderBy(projects.createdAt);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue