feat(gmail): the OAuth client can be entered instead of deployed
The one click that opens Google's consent screen already existed. What it needs behind it is a registered client, and no app can conjure one: Google issues no Gmail access to an unregistered client, and `gmail.readonly` is a restricted scope, so an id shipped inside a public image would be both extractable from it and unusable without Google's security review of every deployment using it. The registration is a one-off. Having to edit a Rune's manifest and recreate the container to paste the result was not. `VERDANDE_GMAIL_CLIENT_ID` and `_SECRET` still win when set. A value in the environment is a deliberate deployment decision, applied again on every recreate, and a form that could quietly override it would make the manifest a lie — so the page says where the value came from, and PUT answers 409. The redirect URI is spelled out on the page rather than left to be reconstructed. It is derived from VERDANDE_BASE_URL, it has to match Google's copy exactly, and the error Google returns when it does not names neither value. Its own table rather than `user_settings`: an OAuth client is the instance's, not one person's — and `user_settings` has `user_id` as its whole primary key, so a second scope there would silently overwrite the AI provider's key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
101ca33069
commit
31bf0557df
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/kristianwind/verdande/internal/auth"
|
||||
@@ -346,3 +347,43 @@ func (db *DB) SetUserSettings(ctx context.Context, userID, scope string, values
|
||||
userID, scope, string(raw), time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
// --- instance-wide settings -------------------------------------------------------------
|
||||
|
||||
// InstanceSettings reads one scope of the instance's own settings. A missing row
|
||||
// is an empty map, not an error.
|
||||
//
|
||||
// Separate from UserSettings because some configuration is the server's rather
|
||||
// than a person's — the Gmail OAuth client is one registration that everybody
|
||||
// connecting a mailbox goes through, not a preference each of them holds.
|
||||
func (db *DB) InstanceSettings(ctx context.Context, scope string) (map[string]any, error) {
|
||||
var raw string
|
||||
err := db.QueryRowContext(ctx,
|
||||
`SELECT values_json FROM instance_settings WHERE scope = ?`, scope).Scan(&raw)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(raw), &values); err != nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (db *DB) SetInstanceSettings(ctx context.Context, scope string, values map[string]any) error {
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.ExecContext(ctx, `
|
||||
INSERT INTO instance_settings (scope, values_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (scope) DO UPDATE SET
|
||||
values_json = excluded.values_json,
|
||||
updated_at = excluded.updated_at`,
|
||||
scope, string(raw), time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user