build and ready for demo

login screen
This commit is contained in:
Daniel Noulet 2026-09-19 17:33:44 +02:00
parent 0ae2dc3888
commit 6af2853faa
33 changed files with 2020 additions and 1221 deletions

View file

@ -1,55 +1,52 @@
package main
import (
"context"
"embed"
"io/fs"
"log"
"net/http"
"time"
"github.com/example/cms/internal/config"
"github.com/example/cms/internal/repository"
"github.com/example/cms/internal/security"
"github.com/example/cms/internal/service"
"github.com/example/cms/internal/web"
"github.com/pressly/goose/v3"
"github.com/pressly/goose/v3/database"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
//go:embed migrations/postgres/*.sql
var migrations embed.FS
// web/dist holds the compiled React app. A placeholder file is committed so the
// embed directive always resolves, even when the frontend has not been built.
//
//go:embed all:web/dist
var ui embed.FS
func main() {
cfg := config.Load()
applicationDB, err := gorm.Open(postgres.Open(cfg.ApplicationDatabaseURL), &gorm.Config{})
content, err := service.NewContentService(service.SeedContent(), cfg.ContentFile)
if err != nil {
log.Fatal(err)
}
applicationSQLDB, err := applicationDB.DB()
assets, err := fs.Sub(ui, "web/dist")
if err != nil {
log.Fatal(err)
}
applicationMigrations, err := fs.Sub(migrations, "migrations/postgres")
if err != nil {
log.Fatal(err)
}
applicationStore, err := database.NewStore(goose.DialectPostgres, "goose_application_db_version")
if err != nil {
log.Fatal(err)
}
applicationProvider, err := goose.NewProvider("", applicationSQLDB, applicationMigrations, goose.WithStore(applicationStore))
if err != nil {
log.Fatal(err)
}
if _, err = applicationProvider.Up(context.Background()); err != nil {
log.Fatal(err)
}
content := service.NewContentService(repository.NewContentRepository(applicationDB))
sso := security.NewSSOTokenVerifier(cfg.SSOHeader, cfg.SSOToken, cfg.SSOCookie)
var demo *service.DemoAuth
if cfg.Profile == "demo" {
demo = service.NewDemoAuth()
log.Printf("CMS profile: demo (in-memory, %d seeded articles)", len(service.SeedContent()))
} else {
log.Printf("CMS profile: %s (SSO identity required; PostgreSQL URL configured: %t)", cfg.Profile, cfg.PostgresURL != "")
}
server := &http.Server{
Addr: cfg.HTTPAddr,
Handler: web.NewHandler(content, assets, sso, demo).Routes(cfg.AllowedOrigins),
ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("CMS listening on %s", cfg.HTTPAddr)
if err = http.ListenAndServe(cfg.HTTPAddr, web.NewHandler(content, sso).Routes(cfg.AllowedOrigins)); err != nil {
if err = server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

View file

@ -1,9 +0,0 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS users (id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', provider TEXT NOT NULL, provider_subject TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_provider_subject ON users(provider, provider_subject);
CREATE TABLE IF NOT EXISTS sessions (id BIGSERIAL PRIMARY KEY, token TEXT NOT NULL UNIQUE, user_id BIGINT REFERENCES users(id), admin_id BIGINT, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
-- +goose Down
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS users;

View file

@ -1,32 +0,0 @@
-- +goose Up
ALTER TABLE users ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN IF NOT EXISTS editor BOOLEAN NOT NULL DEFAULT FALSE;
CREATE TABLE IF NOT EXISTS content (
id BIGSERIAL PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
created_by BIGINT REFERENCES users(id),
updated_by BIGINT REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at DESC, created_at DESC, id DESC);
CREATE TABLE IF NOT EXISTS content_history (
id BIGSERIAL PRIMARY KEY,
content_id BIGINT NOT NULL REFERENCES content(id) ON DELETE CASCADE,
operation TEXT NOT NULL,
actor_id BIGINT REFERENCES users(id),
actor_admin_id BIGINT,
created_at TIMESTAMPTZ NOT NULL,
snapshot JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_content_history_content_id ON content_history(content_id, created_at DESC);
-- +goose Down
DROP TABLE IF EXISTS content_history;
DROP TABLE IF EXISTS content;
ALTER TABLE users DROP COLUMN IF EXISTS editor;
ALTER TABLE users DROP COLUMN IF EXISTS role;

View file

@ -1,26 +0,0 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS categories (
id BIGSERIAL PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
ALTER TABLE content ADD COLUMN IF NOT EXISTS category_id BIGINT REFERENCES categories(id);
CREATE INDEX IF NOT EXISTS idx_content_category_id ON content(category_id);
CREATE TABLE IF NOT EXISTS badges (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS content_badges (
content_id BIGINT NOT NULL REFERENCES content(id) ON DELETE CASCADE,
badge_id BIGINT NOT NULL REFERENCES badges(id) ON DELETE CASCADE,
PRIMARY KEY (content_id, badge_id)
);
CREATE INDEX IF NOT EXISTS idx_content_badges_badge_id ON content_badges(badge_id);
-- +goose Down
DROP TABLE IF EXISTS content_badges;
DROP TABLE IF EXISTS badges;
DROP INDEX IF EXISTS idx_content_category_id;
ALTER TABLE content DROP COLUMN IF EXISTS category_id;
DROP TABLE IF EXISTS categories;

View file

@ -1,35 +0,0 @@
-- +goose Up
-- The CMS no longer stores identities: the upstream SSO proxy owns them.
ALTER TABLE content DROP COLUMN IF EXISTS created_by;
ALTER TABLE content DROP COLUMN IF EXISTS updated_by;
ALTER TABLE content_history DROP COLUMN IF EXISTS actor_id;
ALTER TABLE content_history DROP COLUMN IF EXISTS actor_admin_id;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS users;
-- +goose Down
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL,
provider_subject TEXT NOT NULL,
role TEXT NOT NULL DEFAULT '',
editor BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_provider_subject ON users(provider, provider_subject);
CREATE TABLE IF NOT EXISTS sessions (
id BIGSERIAL PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
user_id BIGINT REFERENCES users(id),
admin_id BIGINT,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
ALTER TABLE content ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users(id);
ALTER TABLE content ADD COLUMN IF NOT EXISTS updated_by BIGINT REFERENCES users(id);
ALTER TABLE content_history ADD COLUMN IF NOT EXISTS actor_id BIGINT REFERENCES users(id);
ALTER TABLE content_history ADD COLUMN IF NOT EXISTS actor_admin_id BIGINT;