feat: add per-widget secret storage for release provider authentication

Adds encrypted token storage per release widget so users can authenticate
with GitHub, GitLab, Docker Hub, etc. without needing full integration setup.

- New `widget_secret` table (all 3 DB drivers) keyed by (itemId, kind)
- tRPC router with setSecret/deleteSecret/getConfiguredKinds (board modify access required)
- Releases router injects decrypted tokens into provider requests (board view access required)
- Tokens only used for repos matching widget's stored configuration (prevents proxy abuse)
- Provider auth: GitHub (Octokit auth), GitLab (PRIVATE-TOKEN), Docker Hub (Bearer),
  GHCR (registry token), npm (Bearer), Codeberg (token header)
- UI: "Provider Tokens" section in widget editor with PasswordInput per provider
This commit is contained in:
Thomas Camlong committed 2026-06-27 10:57:13 +02:00
1 parent d8a7477039
commit 77884a3179
14 files changed
+358 -20

No files matched your search

+27
View File
@@ -530,6 +530,25 @@ export const customWidgetSecrets = sqliteTable(
}),
);
export const widgetSecrets = sqliteTable(
"widget_secret",
{
itemId: text()
.notNull()
.references(() => items.id, { onDelete: "cascade" }),
kind: text().notNull(),
value: text().$type<`${string}.${string}`>().notNull(),
updatedAt: int({ mode: "timestamp" })
.$onUpdateFn(() => new Date())
.notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.itemId, table.kind],
}),
}),
);
export const cronJobConfigurations = sqliteTable("cron_job_configuration", {
name: text().notNull().primaryKey(),
cronExpression: text().notNull(),
@@ -735,12 +754,20 @@ export const sectionCollapseStateRelations = relations(sectionCollapseStates, ({
export const itemRelations = relations(items, ({ one, many }) => ({
integrations: many(integrationItems),
layouts: many(itemLayouts),
secrets: many(widgetSecrets),
board: one(boards, {
fields: [items.boardId],
references: [boards.id],
}),
}));
export const widgetSecretRelations = relations(widgetSecrets, ({ one }) => ({
item: one(items, {
fields: [widgetSecrets.itemId],
references: [items.id],
}),
}));
export const integrationItemRelations = relations(integrationItems, ({ one }) => ({
integration: one(integrations, {
fields: [integrationItems.integrationId],