Core Features: - Complete Prisma database schema with all entities (users, orgs, projects, checks, etc.) - Production-grade authentication service with Argon2 password hashing - JWT-based session management with HttpOnly cookies - Comprehensive auth middleware with role-based access control - RESTful auth API endpoints: register, login, logout, me, refresh - Database seeding with demo data for development - Rate limiting on auth endpoints (5 attempts/15min) Technical Implementation: - Type-safe authentication with Zod validation - Proper error handling and logging throughout - Secure password hashing with Argon2id - JWT tokens with 7-day expiration - Database transactions for atomic operations - Comprehensive middleware for optional/required auth - Role hierarchy system (MEMBER < ADMIN < OWNER) Database Schema: - Users with secure password storage - Organizations with membership management - Projects for organizing redirect checks - Complete audit logging system - API key management for programmatic access - Bulk job tracking for future phases Backward Compatibility: - All existing endpoints preserved and functional - No breaking changes to legacy API responses - New auth system runs alongside existing functionality Ready for Phase 2: Enhanced redirect tracking with database persistence
251 lines
6.9 KiB
Plaintext
251 lines
6.9 KiB
Plaintext
// Redirect Intelligence v2 - Database Schema
|
|
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
output = "../../../node_modules/.prisma/client"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
name String
|
|
passwordHash String @map("password_hash")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
|
|
memberships OrgMembership[]
|
|
auditLogs AuditLog[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Organization {
|
|
id String @id @default(cuid())
|
|
name String
|
|
plan String @default("free")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
memberships OrgMembership[]
|
|
projects Project[]
|
|
apiKeys ApiKey[]
|
|
auditLogs AuditLog[]
|
|
|
|
@@map("organizations")
|
|
}
|
|
|
|
model OrgMembership {
|
|
id String @id @default(cuid())
|
|
orgId String @map("org_id")
|
|
userId String @map("user_id")
|
|
role Role
|
|
|
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([orgId, userId])
|
|
@@map("org_memberships")
|
|
}
|
|
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
orgId String @map("org_id")
|
|
name String
|
|
settingsJson Json @map("settings_json") @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
checks Check[]
|
|
bulkJobs BulkJob[]
|
|
|
|
@@map("projects")
|
|
}
|
|
|
|
model Check {
|
|
id String @id @default(cuid())
|
|
projectId String @map("project_id")
|
|
inputUrl String @map("input_url")
|
|
method String @default("GET")
|
|
headersJson Json @map("headers_json") @default("{}")
|
|
userAgent String? @map("user_agent")
|
|
startedAt DateTime @map("started_at")
|
|
finishedAt DateTime? @map("finished_at")
|
|
status CheckStatus
|
|
finalUrl String? @map("final_url")
|
|
totalTimeMs Int? @map("total_time_ms")
|
|
reportId String? @map("report_id")
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
hops Hop[]
|
|
sslInspections SslInspection[]
|
|
seoFlags SeoFlags?
|
|
securityFlags SecurityFlags?
|
|
reports Report[]
|
|
|
|
@@index([projectId, startedAt(sort: Desc)])
|
|
@@map("checks")
|
|
}
|
|
|
|
model Hop {
|
|
id String @id @default(cuid())
|
|
checkId String @map("check_id")
|
|
hopIndex Int @map("hop_index")
|
|
url String
|
|
scheme String?
|
|
statusCode Int? @map("status_code")
|
|
redirectType RedirectType @map("redirect_type")
|
|
latencyMs Int? @map("latency_ms")
|
|
contentType String? @map("content_type")
|
|
reason String?
|
|
responseHeadersJson Json @map("response_headers_json") @default("{}")
|
|
|
|
check Check @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([checkId, hopIndex])
|
|
@@map("hops")
|
|
}
|
|
|
|
model SslInspection {
|
|
id String @id @default(cuid())
|
|
checkId String @map("check_id")
|
|
host String
|
|
validFrom DateTime? @map("valid_from")
|
|
validTo DateTime? @map("valid_to")
|
|
daysToExpiry Int? @map("days_to_expiry")
|
|
issuer String?
|
|
protocol String?
|
|
warningsJson Json @map("warnings_json") @default("[]")
|
|
|
|
check Check @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("ssl_inspections")
|
|
}
|
|
|
|
model SeoFlags {
|
|
id String @id @default(cuid())
|
|
checkId String @unique @map("check_id")
|
|
robotsTxtStatus String? @map("robots_txt_status")
|
|
robotsTxtRulesJson Json @map("robots_txt_rules_json") @default("{}")
|
|
metaRobots String? @map("meta_robots")
|
|
canonicalUrl String? @map("canonical_url")
|
|
sitemapPresent Boolean @default(false) @map("sitemap_present")
|
|
noindex Boolean @default(false)
|
|
nofollow Boolean @default(false)
|
|
|
|
check Check @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("seo_flags")
|
|
}
|
|
|
|
model SecurityFlags {
|
|
id String @id @default(cuid())
|
|
checkId String @unique @map("check_id")
|
|
safeBrowsingStatus String? @map("safe_browsing_status")
|
|
mixedContent MixedContent @map("mixed_content") @default(NONE)
|
|
httpsToHttp Boolean @map("https_to_http") @default(false)
|
|
|
|
check Check @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("security_flags")
|
|
}
|
|
|
|
model Report {
|
|
id String @id @default(cuid())
|
|
checkId String @map("check_id")
|
|
markdownPath String? @map("markdown_path")
|
|
pdfPath String? @map("pdf_path")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
check Check @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("reports")
|
|
}
|
|
|
|
model BulkJob {
|
|
id String @id @default(cuid())
|
|
projectId String @map("project_id")
|
|
uploadPath String @map("upload_path")
|
|
status JobStatus
|
|
progressJson Json @map("progress_json") @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("bulk_jobs")
|
|
}
|
|
|
|
model ApiKey {
|
|
id String @id @default(cuid())
|
|
orgId String @map("org_id")
|
|
name String
|
|
tokenHash String @unique @map("token_hash")
|
|
permsJson Json @map("perms_json") @default("{}")
|
|
rateLimitQuota Int @map("rate_limit_quota") @default(1000)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tokenHash])
|
|
@@map("api_keys")
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
orgId String @map("org_id")
|
|
actorUserId String? @map("actor_user_id")
|
|
action String
|
|
entity String
|
|
entityId String @map("entity_id")
|
|
metaJson Json @map("meta_json") @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
organization Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
actor User? @relation(fields: [actorUserId], references: [id], onDelete: SetNull)
|
|
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
enum Role {
|
|
OWNER
|
|
ADMIN
|
|
MEMBER
|
|
}
|
|
|
|
enum CheckStatus {
|
|
OK
|
|
ERROR
|
|
TIMEOUT
|
|
LOOP
|
|
}
|
|
|
|
enum RedirectType {
|
|
HTTP_301
|
|
HTTP_302
|
|
HTTP_307
|
|
HTTP_308
|
|
META_REFRESH
|
|
JS
|
|
FINAL
|
|
OTHER
|
|
}
|
|
|
|
enum MixedContent {
|
|
NONE
|
|
PRESENT
|
|
FINAL_TO_HTTP
|
|
}
|
|
|
|
enum JobStatus {
|
|
QUEUED
|
|
RUNNING
|
|
DONE
|
|
ERROR
|
|
}
|