mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
feat: instance administration, bootstrap admin, and managed-tier OAuth gate
Self-hosted instances are now email/password only — OAuth providers (GitHub/Google/Discord/Microsoft) activate solely when PRIMORA_MANAGED=true, reserved for the future hosted tier. Sign-up lifecycle: the first public sign-up becomes the instance admin; after bootstrap, sign-up closes by default. A better-auth databaseHook gates only /sign-up/email, so admin-created users and OAuth flows bypass it. The login page reads GET /instance/public to hide the sign-up tab, show the bootstrap notice, and render only the providers the instance offers. Instance settings live in core.settings (migration 00010) and resolve in-app > env > built-in default, cached 15s. Platform admins (better-auth user.role) manage sign-up, mail transport, and rate limits via GET/PUT/DELETE /instance/settings or the new Settings → Instance tab. Secret values are AES-256-GCM encrypted at rest with PRIMORA_ENCRYPTION_KEY, never returned by the API, and decrypted by the auth service at send time. Auth service reads the same settings table for the sign-up gate and mail config, so in-app changes take effect without a restart. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1214108d6b
commit
c6166de886
36 files changed
+1729
-109
No files matched your search
@@ -336,6 +336,14 @@ type CoreScheduledJobRun struct {
|
||||
FinishedAt pgtype.Timestamptz `json:"finished_at"`
|
||||
}
|
||||
|
||||
type CoreSetting struct {
|
||||
Key string `json:"key"`
|
||||
Value []byte `json:"value"`
|
||||
Secret bool `json:"secret"`
|
||||
UpdatedBy *string `json:"updated_by"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CoreUser struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AuthSubject string `json:"auth_subject"`
|
||||
|
||||
@@ -46,6 +46,7 @@ type Querier interface {
|
||||
DeletePendingInvitationByIDForOrganization(ctx context.Context, arg DeletePendingInvitationByIDForOrganizationParams) (CoreProjectInvitation, error)
|
||||
DeleteProjectByID(ctx context.Context, id uuid.UUID) (CoreProject, error)
|
||||
DeleteScheduledJob(ctx context.Context, arg DeleteScheduledJobParams) (uuid.UUID, error)
|
||||
DeleteSetting(ctx context.Context, key string) error
|
||||
DeleteWebhook(ctx context.Context, arg DeleteWebhookParams) (CoreWebhook, error)
|
||||
EventTotals(ctx context.Context, arg EventTotalsParams) (EventTotalsRow, error)
|
||||
EventTypeSeries(ctx context.Context, arg EventTypeSeriesParams) ([]EventTypeSeriesRow, error)
|
||||
@@ -68,6 +69,7 @@ type Querier interface {
|
||||
GetProjectMembership(ctx context.Context, arg GetProjectMembershipParams) (GetProjectMembershipRow, error)
|
||||
GetProjectOverview(ctx context.Context, id uuid.UUID) (GetProjectOverviewRow, error)
|
||||
GetScheduledJobByID(ctx context.Context, id uuid.UUID) (CoreScheduledJob, error)
|
||||
GetSetting(ctx context.Context, key string) (GetSettingRow, error)
|
||||
GetUserByAuthSubject(ctx context.Context, authSubject string) (CoreUser, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (CoreUser, error)
|
||||
GetWebhookByID(ctx context.Context, id uuid.UUID) (CoreWebhook, error)
|
||||
@@ -97,6 +99,7 @@ type Querier interface {
|
||||
ListProjectsForOrganization(ctx context.Context, arg ListProjectsForOrganizationParams) ([]ListProjectsForOrganizationRow, error)
|
||||
ListScheduledJobRuns(ctx context.Context, arg ListScheduledJobRunsParams) ([]CoreScheduledJobRun, error)
|
||||
ListScheduledJobs(ctx context.Context, projectID uuid.UUID) ([]CoreScheduledJob, error)
|
||||
ListSettings(ctx context.Context) ([]ListSettingsRow, error)
|
||||
ListWebhookDeliveries(ctx context.Context, arg ListWebhookDeliveriesParams) ([]CoreWebhookDelivery, error)
|
||||
ListWebhooks(ctx context.Context, projectID uuid.UUID) ([]CoreWebhook, error)
|
||||
ListWebhooksForEvent(ctx context.Context, arg ListWebhooksForEventParams) ([]CoreWebhook, error)
|
||||
@@ -132,6 +135,7 @@ type Querier interface {
|
||||
UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJobParams) (CoreScheduledJob, error)
|
||||
UpdateWebhook(ctx context.Context, arg UpdateWebhookParams) (CoreWebhook, error)
|
||||
UpsertComponent(ctx context.Context, arg UpsertComponentParams) (CoreComponent, error)
|
||||
UpsertSetting(ctx context.Context, arg UpsertSettingParams) (CoreSetting, error)
|
||||
UpsertUser(ctx context.Context, arg UpsertUserParams) (CoreUser, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: settings.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const deleteSetting = `-- name: DeleteSetting :exec
|
||||
DELETE FROM core.settings WHERE key = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSetting(ctx context.Context, key string) error {
|
||||
_, err := q.db.Exec(ctx, deleteSetting, key)
|
||||
return err
|
||||
}
|
||||
|
||||
const getSetting = `-- name: GetSetting :one
|
||||
SELECT key, value, secret, updated_at FROM core.settings WHERE key = $1
|
||||
`
|
||||
|
||||
type GetSettingRow struct {
|
||||
Key string `json:"key"`
|
||||
Value []byte `json:"value"`
|
||||
Secret bool `json:"secret"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSetting(ctx context.Context, key string) (GetSettingRow, error) {
|
||||
row := q.db.QueryRow(ctx, getSetting, key)
|
||||
var i GetSettingRow
|
||||
err := row.Scan(
|
||||
&i.Key,
|
||||
&i.Value,
|
||||
&i.Secret,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listSettings = `-- name: ListSettings :many
|
||||
SELECT key, value, secret, updated_at FROM core.settings ORDER BY key
|
||||
`
|
||||
|
||||
type ListSettingsRow struct {
|
||||
Key string `json:"key"`
|
||||
Value []byte `json:"value"`
|
||||
Secret bool `json:"secret"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListSettings(ctx context.Context) ([]ListSettingsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListSettingsRow
|
||||
for rows.Next() {
|
||||
var i ListSettingsRow
|
||||
if err := rows.Scan(
|
||||
&i.Key,
|
||||
&i.Value,
|
||||
&i.Secret,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertSetting = `-- name: UpsertSetting :one
|
||||
INSERT INTO core.settings (key, value, secret, updated_by)
|
||||
VALUES ($1, $2::jsonb, $3, $4)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = EXCLUDED.value,
|
||||
secret = EXCLUDED.secret,
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
updated_at = NOW()
|
||||
RETURNING key, value, secret, updated_by, updated_at
|
||||
`
|
||||
|
||||
type UpsertSettingParams struct {
|
||||
Key string `json:"key"`
|
||||
Column2 []byte `json:"column_2"`
|
||||
Secret bool `json:"secret"`
|
||||
UpdatedBy *string `json:"updated_by"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertSetting(ctx context.Context, arg UpsertSettingParams) (CoreSetting, error) {
|
||||
row := q.db.QueryRow(ctx, upsertSetting,
|
||||
arg.Key,
|
||||
arg.Column2,
|
||||
arg.Secret,
|
||||
arg.UpdatedBy,
|
||||
)
|
||||
var i CoreSetting
|
||||
err := row.Scan(
|
||||
&i.Key,
|
||||
&i.Value,
|
||||
&i.Secret,
|
||||
&i.UpdatedBy,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
Reference in new issue
Block a user