mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
This commit bundles two unrelated changes that were swept together by an
accidental `git add -A` in another working session. Documented here so the
history reflects what's actually inside.
═══════════════════════════════════════════════════════════════════════
1. fix(mana-auth): /api/v1/auth/login mints JWT via auth.handler instead
of api.signInEmail
═══════════════════════════════════════════════════════════════════════
Previous attempt (commit 55cc75e7d) tried to fix the broken JWT mint in
/api/v1/auth/login by switching the cookie name from `mana.session_token`
to `__Secure-mana.session_token` for production. That was necessary but
not sufficient: Better Auth's session cookie value isn't just the raw
session token, it's `<token>.<HMAC>` where the HMAC is derived from the
better-auth secret. Reconstructing the cookie from auth.api.signInEmail's
JSON response only gave us the raw token, so /api/auth/token's
get-session middleware still couldn't validate it and the JWT mint kept
silently failing.
Real fix: do the sign-in via auth.handler (the HTTP path) rather than
auth.api.signInEmail (the SDK path). The handler returns a real fetch
Response with a Set-Cookie header containing the fully signed cookie
envelope. We capture that header verbatim and forward it as the cookie
on the /api/auth/token request, which now passes validation and mints
the JWT correctly.
Verified end-to-end on auth.mana.how:
$ curl -X POST https://auth.mana.how/api/v1/auth/login \
-d '{"email":"...","password":"..."}'
{
"user": {...},
"token": "<session token>",
"accessToken": "eyJhbGciOiJFZERTQSI...", ← real JWT now
"refreshToken": "<session token>"
}
Side benefits:
- Email-not-verified path is now handled by checking
signInResponse.status === 403 directly, no more catching APIError
with the comment-noted async-stream footgun.
- X-Forwarded-For is forwarded explicitly so Better Auth's rate limiter
and our security log see the real client IP.
- The leftover catch block now only handles unexpected exceptions
(network errors etc); the FORBIDDEN-checking logic in it is dead but
harmless and left in for defense in depth.
═══════════════════════════════════════════════════════════════════════
2. chore: remove the entire self-hosted Matrix stack (Synapse, Element,
Manalink, mana-matrix-bot)
═══════════════════════════════════════════════════════════════════════
The Matrix subsystem ran parallel to the main Mana product without any
load-bearing integration: the unified web app never imported matrix-js-sdk,
the chat module uses mana-sync (local-first), and mana-matrix-bot's
plugins duplicated features the unified app already ships natively.
Keeping it alive cost a Synapse + Element + matrix-web + bot container
quartet, three Cloudflare routes, an OIDC provider plugin in mana-auth,
and a steady drip of devlog/dependency churn.
Removed:
- apps/matrix (Manalink web + mobile, ~150 files)
- services/mana-matrix-bot (Go bot with ~20 plugins)
- docker/matrix configs (Synapse + Element)
- synapse/element-web/matrix-web/mana-matrix-bot services in
docker-compose.macmini.yml
- matrix.mana.how/element.mana.how/link.mana.how Cloudflare tunnel routes
- OIDC provider plugin + matrix-synapse trustedClient + matrixUserLinks
table from mana-auth (oauth_* schema definitions also removed)
- MatrixService import path in mana-media (importFromMatrix endpoint)
- Matrix notification channel in mana-notify (worker, metrics, config,
channel_type enum, MatrixOptions handler)
- Matrix entries from shared-branding (mana-apps + app-icons),
notify-client, the i18n bundle, the observatory map, the credits
app-label list, the landing footer/apps page, the prometheus + alerts
+ promtail tier mappings, and the matrix-related deploy paths in
cd-macmini.yml + ci.yml
Devlog/manascore/blueprint entries that mention Matrix are left intact
as historical record. The oauth_* + matrix_user_links Postgres tables
stay on existing prod databases — code can no longer write to them, drop
them in a follow-up migration if you want them gone for real.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
)
|
|
|
|
func (d *DB) migrate(ctx context.Context) error {
|
|
slog.Info("running database migrations")
|
|
|
|
migrations := []string{
|
|
`CREATE SCHEMA IF NOT EXISTS notify`,
|
|
|
|
// Enum types (idempotent with DO blocks)
|
|
`DO $$ BEGIN
|
|
CREATE TYPE notify.channel_type AS ENUM ('email', 'push', 'webhook');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$`,
|
|
|
|
`DO $$ BEGIN
|
|
CREATE TYPE notify.notification_status AS ENUM ('pending', 'processing', 'delivered', 'failed', 'cancelled');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$`,
|
|
|
|
`DO $$ BEGIN
|
|
CREATE TYPE notify.priority_type AS ENUM ('low', 'normal', 'high', 'critical');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$`,
|
|
|
|
// Notifications table
|
|
`CREATE TABLE IF NOT EXISTS notify.notifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT,
|
|
app_id VARCHAR(50) NOT NULL,
|
|
channel notify.channel_type NOT NULL,
|
|
template_id VARCHAR(100),
|
|
subject VARCHAR(500),
|
|
body TEXT,
|
|
data JSONB,
|
|
status notify.notification_status NOT NULL DEFAULT 'pending',
|
|
priority notify.priority_type NOT NULL DEFAULT 'normal',
|
|
scheduled_for TIMESTAMPTZ,
|
|
recipient VARCHAR(500),
|
|
external_id VARCHAR(255),
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
delivered_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notify.notifications (user_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_notifications_app_id ON notify.notifications (app_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_notifications_status ON notify.notifications (status)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_notifications_scheduled_for ON notify.notifications (scheduled_for)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_notifications_external_id ON notify.notifications (external_id)`,
|
|
|
|
// Templates table
|
|
`CREATE TABLE IF NOT EXISTS notify.templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
slug VARCHAR(100) NOT NULL,
|
|
app_id VARCHAR(50),
|
|
channel notify.channel_type NOT NULL,
|
|
subject VARCHAR(500),
|
|
body_template TEXT NOT NULL,
|
|
locale VARCHAR(10) NOT NULL DEFAULT 'de-DE',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
is_system BOOLEAN NOT NULL DEFAULT false,
|
|
variables JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE(slug, locale)
|
|
)`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_templates_app_id ON notify.templates (app_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_templates_channel ON notify.templates (channel)`,
|
|
|
|
// Devices table
|
|
`CREATE TABLE IF NOT EXISTS notify.devices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
push_token TEXT NOT NULL UNIQUE,
|
|
token_type VARCHAR(20) NOT NULL DEFAULT 'expo',
|
|
platform VARCHAR(20),
|
|
device_name VARCHAR(100),
|
|
app_id VARCHAR(50),
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
last_seen_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_devices_user_id ON notify.devices (user_id)`,
|
|
|
|
// Preferences table
|
|
`CREATE TABLE IF NOT EXISTS notify.preferences (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL UNIQUE,
|
|
email_enabled BOOLEAN NOT NULL DEFAULT false,
|
|
push_enabled BOOLEAN NOT NULL DEFAULT true,
|
|
quiet_hours_enabled BOOLEAN NOT NULL DEFAULT false,
|
|
quiet_hours_start VARCHAR(5),
|
|
quiet_hours_end VARCHAR(5),
|
|
timezone VARCHAR(50) NOT NULL DEFAULT 'Europe/Berlin',
|
|
category_preferences JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)`,
|
|
|
|
// Delivery logs table
|
|
`CREATE TABLE IF NOT EXISTS notify.delivery_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
notification_id UUID NOT NULL REFERENCES notify.notifications(id) ON DELETE CASCADE,
|
|
attempt_number INTEGER NOT NULL,
|
|
channel notify.channel_type NOT NULL,
|
|
success BOOLEAN NOT NULL,
|
|
status_code INTEGER,
|
|
error_message TEXT,
|
|
provider_id VARCHAR(255),
|
|
duration_ms INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)`,
|
|
|
|
`CREATE INDEX IF NOT EXISTS idx_delivery_logs_notification_id ON notify.delivery_logs (notification_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_delivery_logs_success ON notify.delivery_logs (success)`,
|
|
}
|
|
|
|
for _, sql := range migrations {
|
|
if _, err := d.Pool.Exec(ctx, sql); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
slog.Info("migrations completed")
|
|
return nil
|
|
}
|