mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 11:36:44 +03:00
697 lines
25 KiB
TypeScript
697 lines
25 KiB
TypeScript
import { z } from "zod";
|
|
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
import { sql } from "drizzle-orm";
|
|
|
|
// ===== Zod Validation Schemas =====
|
|
export const userSchema = z.object({
|
|
id: z.string(),
|
|
username: z.string(),
|
|
password: z.string(),
|
|
email: z.email(),
|
|
emailVerified: z.boolean().default(false),
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
});
|
|
|
|
export const githubConfigSchema = z.object({
|
|
owner: z.string(),
|
|
type: z.enum(["personal", "organization"]),
|
|
token: z.string(),
|
|
includeStarred: z.boolean().default(false),
|
|
includeForks: z.boolean().default(true),
|
|
skipForks: z.boolean().default(false),
|
|
includeArchived: z.boolean().default(false),
|
|
includePrivate: z.boolean().default(true),
|
|
includePublic: z.boolean().default(true),
|
|
includeOrganizations: z.array(z.string()).default([]),
|
|
starredReposOrg: z.string().optional(),
|
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
|
defaultOrg: z.string().optional(),
|
|
starredCodeOnly: z.boolean().default(false),
|
|
skipStarredIssues: z.boolean().optional(), // Deprecated: kept for backward compatibility, use starredCodeOnly instead
|
|
starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(),
|
|
});
|
|
|
|
export const giteaConfigSchema = z.object({
|
|
url: z.url(),
|
|
token: z.string(),
|
|
defaultOwner: z.string(),
|
|
organization: z.string().optional(),
|
|
mirrorInterval: z.string().default("8h"),
|
|
lfs: z.boolean().default(false),
|
|
wiki: z.boolean().default(false),
|
|
visibility: z
|
|
.enum(["public", "private", "limited", "default"])
|
|
.default("default"),
|
|
createOrg: z.boolean().default(true),
|
|
templateOwner: z.string().optional(),
|
|
templateRepo: z.string().optional(),
|
|
addTopics: z.boolean().default(true),
|
|
topicPrefix: z.string().optional(),
|
|
preserveVisibility: z.boolean().default(true),
|
|
preserveOrgStructure: z.boolean().default(false),
|
|
forkStrategy: z
|
|
.enum(["skip", "reference", "full-copy"])
|
|
.default("reference"),
|
|
// Mirror options
|
|
issueConcurrency: z.number().int().min(1).default(3),
|
|
pullRequestConcurrency: z.number().int().min(1).default(5),
|
|
mirrorReleases: z.boolean().default(false),
|
|
releaseLimit: z.number().default(10),
|
|
mirrorMetadata: z.boolean().default(false),
|
|
mirrorIssues: z.boolean().default(false),
|
|
mirrorPullRequests: z.boolean().default(false),
|
|
mirrorLabels: z.boolean().default(false),
|
|
mirrorMilestones: z.boolean().default(false),
|
|
});
|
|
|
|
export const scheduleConfigSchema = z.object({
|
|
enabled: z.boolean().default(false),
|
|
interval: z.string().default("0 2 * * *"),
|
|
concurrent: z.boolean().default(false),
|
|
batchSize: z.number().default(10),
|
|
pauseBetweenBatches: z.number().default(5000),
|
|
retryAttempts: z.number().default(3),
|
|
retryDelay: z.number().default(60000),
|
|
timeout: z.number().default(3600000),
|
|
autoRetry: z.boolean().default(true),
|
|
cleanupBeforeMirror: z.boolean().default(false),
|
|
notifyOnFailure: z.boolean().default(true),
|
|
notifyOnSuccess: z.boolean().default(false),
|
|
logLevel: z.enum(["error", "warn", "info", "debug"]).default("info"),
|
|
timezone: z.string().default("UTC"),
|
|
onlyMirrorUpdated: z.boolean().default(false),
|
|
updateInterval: z.number().default(86400000),
|
|
skipRecentlyMirrored: z.boolean().default(true),
|
|
recentThreshold: z.number().default(3600000),
|
|
autoImport: z.boolean().default(true),
|
|
autoMirror: z.boolean().default(false),
|
|
lastRun: z.coerce.date().optional(),
|
|
nextRun: z.coerce.date().optional(),
|
|
});
|
|
|
|
export const cleanupConfigSchema = z.object({
|
|
enabled: z.boolean().default(false),
|
|
retentionDays: z.number().default(604800), // 7 days in seconds
|
|
deleteFromGitea: z.boolean().default(false),
|
|
deleteIfNotInGitHub: z.boolean().default(true),
|
|
protectedRepos: z.array(z.string()).default([]),
|
|
dryRun: z.boolean().default(false),
|
|
orphanedRepoAction: z
|
|
.enum(["skip", "archive", "delete"])
|
|
.default("archive"),
|
|
batchSize: z.number().default(10),
|
|
pauseBetweenDeletes: z.number().default(2000),
|
|
lastRun: z.coerce.date().optional(),
|
|
nextRun: z.coerce.date().optional(),
|
|
});
|
|
|
|
export const configSchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
name: z.string(),
|
|
isActive: z.boolean().default(true),
|
|
githubConfig: githubConfigSchema,
|
|
giteaConfig: giteaConfigSchema,
|
|
include: z.array(z.string()).default(["*"]),
|
|
exclude: z.array(z.string()).default([]),
|
|
scheduleConfig: scheduleConfigSchema,
|
|
cleanupConfig: cleanupConfigSchema,
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
});
|
|
|
|
export const repositorySchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
configId: z.string(),
|
|
name: z.string(),
|
|
fullName: z.string(),
|
|
normalizedFullName: z.string(),
|
|
url: z.url(),
|
|
cloneUrl: z.url(),
|
|
owner: z.string(),
|
|
organization: z.string().optional().nullable(),
|
|
mirroredLocation: z.string().default(""),
|
|
isPrivate: z.boolean().default(false),
|
|
isForked: z.boolean().default(false),
|
|
forkedFrom: z.string().optional().nullable(),
|
|
hasIssues: z.boolean().default(false),
|
|
isStarred: z.boolean().default(false),
|
|
isArchived: z.boolean().default(false),
|
|
size: z.number().default(0),
|
|
hasLFS: z.boolean().default(false),
|
|
hasSubmodules: z.boolean().default(false),
|
|
language: z.string().optional().nullable(),
|
|
description: z.string().optional().nullable(),
|
|
defaultBranch: z.string(),
|
|
visibility: z.enum(["public", "private", "internal"]).default("public"),
|
|
status: z
|
|
.enum([
|
|
"imported",
|
|
"mirroring",
|
|
"mirrored",
|
|
"failed",
|
|
"skipped",
|
|
"ignored", // User explicitly wants to ignore this repository
|
|
"deleting",
|
|
"deleted",
|
|
"syncing",
|
|
"synced",
|
|
"archived",
|
|
])
|
|
.default("imported"),
|
|
lastMirrored: z.coerce.date().optional().nullable(),
|
|
errorMessage: z.string().optional().nullable(),
|
|
destinationOrg: z.string().optional().nullable(),
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
});
|
|
|
|
export const mirrorJobSchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
repositoryId: z.string().optional().nullable(),
|
|
repositoryName: z.string().optional().nullable(),
|
|
organizationId: z.string().optional().nullable(),
|
|
organizationName: z.string().optional().nullable(),
|
|
details: z.string().optional().nullable(),
|
|
status: z
|
|
.enum([
|
|
"imported",
|
|
"mirroring",
|
|
"mirrored",
|
|
"failed",
|
|
"skipped",
|
|
"ignored", // User explicitly wants to ignore this repository
|
|
"deleting",
|
|
"deleted",
|
|
"syncing",
|
|
"synced",
|
|
"archived",
|
|
])
|
|
.default("imported"),
|
|
message: z.string(),
|
|
timestamp: z.coerce.date(),
|
|
jobType: z.enum(["mirror", "cleanup", "import"]).default("mirror"),
|
|
batchId: z.string().optional().nullable(),
|
|
totalItems: z.number().optional().nullable(),
|
|
completedItems: z.number().default(0),
|
|
itemIds: z.array(z.string()).optional().nullable(),
|
|
completedItemIds: z.array(z.string()).default([]),
|
|
inProgress: z.boolean().default(false),
|
|
startedAt: z.coerce.date().optional().nullable(),
|
|
completedAt: z.coerce.date().optional().nullable(),
|
|
lastCheckpoint: z.coerce.date().optional().nullable(),
|
|
});
|
|
|
|
export const organizationSchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
configId: z.string(),
|
|
name: z.string(),
|
|
normalizedName: z.string(),
|
|
avatarUrl: z.string(),
|
|
membershipRole: z.enum(["member", "admin", "owner", "billing_manager"]).default("member"),
|
|
isIncluded: z.boolean().default(true),
|
|
destinationOrg: z.string().optional().nullable(),
|
|
status: z
|
|
.enum([
|
|
"imported",
|
|
"mirroring",
|
|
"mirrored",
|
|
"failed",
|
|
"skipped",
|
|
"ignored", // User explicitly wants to ignore this repository
|
|
"deleting",
|
|
"deleted",
|
|
"syncing",
|
|
"synced",
|
|
])
|
|
.default("imported"),
|
|
lastMirrored: z.coerce.date().optional().nullable(),
|
|
errorMessage: z.string().optional().nullable(),
|
|
repositoryCount: z.number().default(0),
|
|
publicRepositoryCount: z.number().optional(),
|
|
privateRepositoryCount: z.number().optional(),
|
|
forkRepositoryCount: z.number().optional(),
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
});
|
|
|
|
export const eventSchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
channel: z.string(),
|
|
payload: z.any(),
|
|
read: z.boolean().default(false),
|
|
createdAt: z.coerce.date(),
|
|
});
|
|
|
|
// ===== Drizzle Table Definitions =====
|
|
|
|
export const users = sqliteTable("users", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name"),
|
|
email: text("email").notNull().unique(),
|
|
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
|
|
image: text("image"),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
// Custom fields
|
|
username: text("username"),
|
|
}, (_table) => []);
|
|
|
|
export const events = sqliteTable("events", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
channel: text("channel").notNull(),
|
|
payload: text("payload", { mode: "json" }).notNull(),
|
|
read: integer("read", { mode: "boolean" }).notNull().default(false),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_events_user_channel").on(table.userId, table.channel),
|
|
index("idx_events_created_at").on(table.createdAt),
|
|
index("idx_events_read").on(table.read),
|
|
]);
|
|
|
|
export const configs = sqliteTable("configs", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
name: text("name").notNull(),
|
|
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
|
|
|
githubConfig: text("github_config", { mode: "json" })
|
|
.$type<z.infer<typeof githubConfigSchema>>()
|
|
.notNull(),
|
|
|
|
giteaConfig: text("gitea_config", { mode: "json" })
|
|
.$type<z.infer<typeof giteaConfigSchema>>()
|
|
.notNull(),
|
|
|
|
include: text("include", { mode: "json" })
|
|
.$type<string[]>()
|
|
.notNull()
|
|
.default(sql`'["*"]'`),
|
|
|
|
exclude: text("exclude", { mode: "json" })
|
|
.$type<string[]>()
|
|
.notNull()
|
|
.default(sql`'[]'`),
|
|
|
|
scheduleConfig: text("schedule_config", { mode: "json" })
|
|
.$type<z.infer<typeof scheduleConfigSchema>>()
|
|
.notNull(),
|
|
|
|
cleanupConfig: text("cleanup_config", { mode: "json" })
|
|
.$type<z.infer<typeof cleanupConfigSchema>>()
|
|
.notNull(),
|
|
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (_table) => []);
|
|
|
|
export const repositories = sqliteTable("repositories", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
configId: text("config_id")
|
|
.notNull()
|
|
.references(() => configs.id),
|
|
name: text("name").notNull(),
|
|
fullName: text("full_name").notNull(),
|
|
normalizedFullName: text("normalized_full_name").notNull(),
|
|
url: text("url").notNull(),
|
|
cloneUrl: text("clone_url").notNull(),
|
|
owner: text("owner").notNull(),
|
|
organization: text("organization"),
|
|
mirroredLocation: text("mirrored_location").default(""),
|
|
|
|
isPrivate: integer("is_private", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false),
|
|
forkedFrom: text("forked_from"),
|
|
|
|
hasIssues: integer("has_issues", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
isStarred: integer("is_starred", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
isArchived: integer("is_archived", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
|
|
size: integer("size").notNull().default(0),
|
|
hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false),
|
|
hasSubmodules: integer("has_submodules", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
|
|
language: text("language"),
|
|
description: text("description"),
|
|
defaultBranch: text("default_branch").notNull(),
|
|
visibility: text("visibility").notNull().default("public"),
|
|
|
|
status: text("status").notNull().default("imported"),
|
|
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
|
errorMessage: text("error_message"),
|
|
|
|
destinationOrg: text("destination_org"),
|
|
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_repositories_user_id").on(table.userId),
|
|
index("idx_repositories_config_id").on(table.configId),
|
|
index("idx_repositories_status").on(table.status),
|
|
index("idx_repositories_owner").on(table.owner),
|
|
index("idx_repositories_organization").on(table.organization),
|
|
index("idx_repositories_is_fork").on(table.isForked),
|
|
index("idx_repositories_is_starred").on(table.isStarred),
|
|
uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName),
|
|
uniqueIndex("uniq_repositories_user_normalized_full_name").on(table.userId, table.normalizedFullName),
|
|
]);
|
|
|
|
export const mirrorJobs = sqliteTable("mirror_jobs", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
repositoryId: text("repository_id"),
|
|
repositoryName: text("repository_name"),
|
|
organizationId: text("organization_id"),
|
|
organizationName: text("organization_name"),
|
|
details: text("details"),
|
|
status: text("status").notNull().default("imported"),
|
|
message: text("message").notNull(),
|
|
timestamp: integer("timestamp", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
|
|
// Job resilience fields
|
|
jobType: text("job_type").notNull().default("mirror"),
|
|
batchId: text("batch_id"),
|
|
totalItems: integer("total_items"),
|
|
completedItems: integer("completed_items").default(0),
|
|
itemIds: text("item_ids", { mode: "json" }).$type<string[]>(),
|
|
completedItemIds: text("completed_item_ids", { mode: "json" })
|
|
.$type<string[]>()
|
|
.default(sql`'[]'`),
|
|
inProgress: integer("in_progress", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
startedAt: integer("started_at", { mode: "timestamp" }),
|
|
completedAt: integer("completed_at", { mode: "timestamp" }),
|
|
lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }),
|
|
}, (table) => [
|
|
index("idx_mirror_jobs_user_id").on(table.userId),
|
|
index("idx_mirror_jobs_batch_id").on(table.batchId),
|
|
index("idx_mirror_jobs_in_progress").on(table.inProgress),
|
|
index("idx_mirror_jobs_job_type").on(table.jobType),
|
|
index("idx_mirror_jobs_timestamp").on(table.timestamp),
|
|
]);
|
|
|
|
export const organizations = sqliteTable("organizations", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
configId: text("config_id")
|
|
.notNull()
|
|
.references(() => configs.id),
|
|
name: text("name").notNull(),
|
|
normalizedName: text("normalized_name").notNull(),
|
|
|
|
avatarUrl: text("avatar_url").notNull(),
|
|
|
|
membershipRole: text("membership_role").notNull().default("member"),
|
|
|
|
isIncluded: integer("is_included", { mode: "boolean" })
|
|
.notNull()
|
|
.default(true),
|
|
|
|
destinationOrg: text("destination_org"),
|
|
|
|
status: text("status").notNull().default("imported"),
|
|
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
|
errorMessage: text("error_message"),
|
|
|
|
repositoryCount: integer("repository_count").notNull().default(0),
|
|
publicRepositoryCount: integer("public_repository_count"),
|
|
privateRepositoryCount: integer("private_repository_count"),
|
|
forkRepositoryCount: integer("fork_repository_count"),
|
|
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_organizations_user_id").on(table.userId),
|
|
index("idx_organizations_config_id").on(table.configId),
|
|
index("idx_organizations_status").on(table.status),
|
|
index("idx_organizations_is_included").on(table.isIncluded),
|
|
uniqueIndex("uniq_organizations_user_normalized_name").on(table.userId, table.normalizedName),
|
|
]);
|
|
|
|
// ===== Better Auth Tables =====
|
|
|
|
// Sessions table
|
|
export const sessions = sqliteTable("sessions", {
|
|
id: text("id").primaryKey(),
|
|
token: text("token").notNull().unique(),
|
|
userId: text("user_id").notNull().references(() => users.id),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
ipAddress: text("ip_address"),
|
|
userAgent: text("user_agent"),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_sessions_user_id").on(table.userId),
|
|
index("idx_sessions_token").on(table.token),
|
|
index("idx_sessions_expires_at").on(table.expiresAt),
|
|
]);
|
|
|
|
// Accounts table (for OAuth providers and credentials)
|
|
export const accounts = sqliteTable("accounts", {
|
|
id: text("id").primaryKey(),
|
|
accountId: text("account_id").notNull(),
|
|
userId: text("user_id").notNull().references(() => users.id),
|
|
providerId: text("provider_id").notNull(),
|
|
providerUserId: text("provider_user_id"), // Make nullable for email/password auth
|
|
accessToken: text("access_token"),
|
|
refreshToken: text("refresh_token"),
|
|
idToken: text("id_token"),
|
|
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
|
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
|
|
scope: text("scope"),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
|
password: text("password"), // For credential provider
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_accounts_account_id").on(table.accountId),
|
|
index("idx_accounts_user_id").on(table.userId),
|
|
index("idx_accounts_provider").on(table.providerId, table.providerUserId),
|
|
]);
|
|
|
|
// Verification tokens table
|
|
export const verificationTokens = sqliteTable("verification_tokens", {
|
|
id: text("id").primaryKey(),
|
|
token: text("token").notNull().unique(),
|
|
identifier: text("identifier").notNull(),
|
|
type: text("type").notNull(), // email, password-reset, etc
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_verification_tokens_token").on(table.token),
|
|
index("idx_verification_tokens_identifier").on(table.identifier),
|
|
]);
|
|
|
|
// Verifications table (for Better Auth)
|
|
export const verifications = sqliteTable("verifications", {
|
|
id: text("id").primaryKey(),
|
|
identifier: text("identifier").notNull(),
|
|
value: text("value").notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_verifications_identifier").on(table.identifier),
|
|
]);
|
|
|
|
// ===== OIDC Provider Tables =====
|
|
|
|
// OAuth Applications table
|
|
export const oauthApplications = sqliteTable("oauth_applications", {
|
|
id: text("id").primaryKey(),
|
|
clientId: text("client_id").notNull().unique(),
|
|
clientSecret: text("client_secret").notNull(),
|
|
name: text("name").notNull(),
|
|
redirectURLs: text("redirect_urls").notNull(), // Comma-separated list
|
|
metadata: text("metadata"), // JSON string
|
|
type: text("type").notNull(), // web, mobile, etc
|
|
disabled: integer("disabled", { mode: "boolean" }).notNull().default(false),
|
|
userId: text("user_id"), // Optional - owner of the application
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_oauth_applications_client_id").on(table.clientId),
|
|
index("idx_oauth_applications_user_id").on(table.userId),
|
|
]);
|
|
|
|
// OAuth Access Tokens table
|
|
export const oauthAccessTokens = sqliteTable("oauth_access_tokens", {
|
|
id: text("id").primaryKey(),
|
|
accessToken: text("access_token").notNull(),
|
|
refreshToken: text("refresh_token"),
|
|
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }).notNull(),
|
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
|
|
clientId: text("client_id").notNull(),
|
|
userId: text("user_id").notNull().references(() => users.id),
|
|
scopes: text("scopes").notNull(), // Comma-separated list
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_oauth_access_tokens_access_token").on(table.accessToken),
|
|
index("idx_oauth_access_tokens_user_id").on(table.userId),
|
|
index("idx_oauth_access_tokens_client_id").on(table.clientId),
|
|
]);
|
|
|
|
// OAuth Consent table
|
|
export const oauthConsent = sqliteTable("oauth_consent", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id),
|
|
clientId: text("client_id").notNull(),
|
|
scopes: text("scopes").notNull(), // Comma-separated list
|
|
consentGiven: integer("consent_given", { mode: "boolean" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_oauth_consent_user_id").on(table.userId),
|
|
index("idx_oauth_consent_client_id").on(table.clientId),
|
|
index("idx_oauth_consent_user_client").on(table.userId, table.clientId),
|
|
]);
|
|
|
|
// ===== SSO Provider Tables =====
|
|
|
|
// SSO Providers table
|
|
export const ssoProviders = sqliteTable("sso_providers", {
|
|
id: text("id").primaryKey(),
|
|
issuer: text("issuer").notNull(),
|
|
domain: text("domain").notNull(),
|
|
oidcConfig: text("oidc_config").notNull(), // JSON string with OIDC configuration
|
|
userId: text("user_id").notNull(), // Admin who created this provider
|
|
providerId: text("provider_id").notNull().unique(), // Unique identifier for the provider
|
|
organizationId: text("organization_id"), // Optional - if provider is linked to an organization
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_sso_providers_provider_id").on(table.providerId),
|
|
index("idx_sso_providers_domain").on(table.domain),
|
|
index("idx_sso_providers_issuer").on(table.issuer),
|
|
]);
|
|
|
|
// ===== Rate Limit Tracking =====
|
|
|
|
export const rateLimitSchema = z.object({
|
|
id: z.string(),
|
|
userId: z.string(),
|
|
provider: z.enum(["github", "gitea"]).default("github"),
|
|
limit: z.number(),
|
|
remaining: z.number(),
|
|
used: z.number(),
|
|
reset: z.coerce.date(),
|
|
retryAfter: z.number().optional(), // seconds to wait
|
|
status: z.enum(["ok", "warning", "limited", "exceeded"]).default("ok"),
|
|
lastChecked: z.coerce.date(),
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
});
|
|
|
|
export const rateLimits = sqliteTable("rate_limits", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
provider: text("provider").notNull().default("github"),
|
|
limit: integer("limit").notNull(),
|
|
remaining: integer("remaining").notNull(),
|
|
used: integer("used").notNull(),
|
|
reset: integer("reset", { mode: "timestamp" }).notNull(),
|
|
retryAfter: integer("retry_after"), // seconds to wait
|
|
status: text("status").notNull().default("ok"),
|
|
lastChecked: integer("last_checked", { mode: "timestamp" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.default(sql`(unixepoch())`),
|
|
}, (table) => [
|
|
index("idx_rate_limits_user_provider").on(table.userId, table.provider),
|
|
index("idx_rate_limits_status").on(table.status),
|
|
]);
|
|
|
|
// Export type definitions
|
|
export type User = z.infer<typeof userSchema>;
|
|
export type Config = z.infer<typeof configSchema>;
|
|
export type Repository = z.infer<typeof repositorySchema>;
|
|
export type MirrorJob = z.infer<typeof mirrorJobSchema>;
|
|
export type Organization = z.infer<typeof organizationSchema>;
|
|
export type Event = z.infer<typeof eventSchema>;
|
|
export type RateLimit = z.infer<typeof rateLimitSchema>;
|