list() {
+ return posts.tagCounts().stream()
+ .map(row -> new Dto.TagCount((String) row[0], ((Number) row[1]).longValue()))
+ .toList();
+ }
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
new file mode 100644
index 0000000..a98d342
--- /dev/null
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,56 @@
+spring:
+ application:
+ name: confessions-of-grace
+ datasource:
+ url: ${DB_URL:jdbc:postgresql://localhost:5432/confessions_of_grace}
+ username: ${DB_USER:confessions_of_grace}
+ password: ${DB_PASSWORD:changeme}
+ jpa:
+ hibernate:
+ ddl-auto: validate
+ open-in-view: false
+ flyway:
+ enabled: true
+
+platform:
+ web:
+ spa:
+ enabled: true
+ data:
+ auditing:
+ enabled: true
+ storage:
+ endpoint: ${S3_ENDPOINT:https://s3.thebennett.net}
+ access-key: ${S3_ACCESS_KEY:}
+ secret-key: ${S3_SECRET_KEY:}
+ path-style-access: true
+ security:
+ # Public site: everything is readable/commentable/subscribable without login; only /api/admin/** needs
+ # an Authentik login. mode=OIDC is set in the deploy env (tests default to NONE).
+ permit-paths:
+ - /
+ - /index.html
+ - /assets/**
+ - /favicon.ico
+ - /actuator/health/**
+ - /api/me
+ - /api/posts/**
+ - /api/tags/**
+ - /api/authors/**
+ - /api/comments/**
+ - /api/subscriptions/**
+ - /api/confession/**
+
+bennett:
+ storage:
+ bucket: ${COG_BUCKET:confessions-of-grace}
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info
+ endpoint:
+ health:
+ probes:
+ enabled: true
diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql
new file mode 100644
index 0000000..9b1c71b
--- /dev/null
+++ b/src/main/resources/db/migration/V1__init.sql
@@ -0,0 +1,51 @@
+create table author (
+ id bigint generated by default as identity primary key,
+ name varchar(120) not null unique,
+ bio text not null default '',
+ x_link varchar(255),
+ fb_link varchar(255),
+ insta_link varchar(255),
+ pfp_link varchar(255),
+ created_at timestamptz,
+ updated_at timestamptz
+);
+
+create table post (
+ id bigint generated by default as identity primary key,
+ slug varchar(200) not null unique,
+ title varchar(300) not null,
+ excerpt text not null default '',
+ author varchar(120) not null default '',
+ cover_image varchar(255),
+ published_on date not null,
+ content text not null default '', -- raw markdown (body)
+ content_html text not null default '', -- rendered HTML
+ published boolean not null default true,
+ created_at timestamptz,
+ updated_at timestamptz
+);
+create index idx_post_pub on post (published, published_on desc);
+
+create table post_tags (
+ post_id bigint not null references post (id) on delete cascade,
+ tag varchar(80) not null
+);
+create index idx_post_tags_post on post_tags (post_id);
+
+create table comment (
+ id bigint generated by default as identity primary key,
+ post_slug varchar(200) not null,
+ name varchar(120) not null,
+ email varchar(200) not null,
+ body text not null,
+ created_at timestamptz,
+ updated_at timestamptz
+);
+create index idx_comment_post on comment (post_slug, created_at);
+
+create table subscription (
+ id bigint generated by default as identity primary key,
+ email varchar(200) not null unique,
+ created_at timestamptz,
+ updated_at timestamptz
+);
diff --git a/data/posts/25-books.md b/src/main/resources/seed/posts/25-books.md
similarity index 100%
rename from data/posts/25-books.md
rename to src/main/resources/seed/posts/25-books.md
diff --git a/data/posts/a-certain-sound.md b/src/main/resources/seed/posts/a-certain-sound.md
similarity index 100%
rename from data/posts/a-certain-sound.md
rename to src/main/resources/seed/posts/a-certain-sound.md
diff --git a/data/posts/christian_nationalism.md b/src/main/resources/seed/posts/christian_nationalism.md
similarity index 100%
rename from data/posts/christian_nationalism.md
rename to src/main/resources/seed/posts/christian_nationalism.md
diff --git a/data/posts/fourfold.md b/src/main/resources/seed/posts/fourfold.md
similarity index 100%
rename from data/posts/fourfold.md
rename to src/main/resources/seed/posts/fourfold.md
diff --git a/data/posts/the-eternity-of-our-bodies.md b/src/main/resources/seed/posts/the-eternity-of-our-bodies.md
similarity index 100%
rename from data/posts/the-eternity-of-our-bodies.md
rename to src/main/resources/seed/posts/the-eternity-of-our-bodies.md
diff --git a/data/posts/vov-adoration.md b/src/main/resources/seed/posts/vov-adoration.md
similarity index 100%
rename from data/posts/vov-adoration.md
rename to src/main/resources/seed/posts/vov-adoration.md
diff --git a/src/test/java/net/reformedwitness/cog/ConfessionsApplicationTests.java b/src/test/java/net/reformedwitness/cog/ConfessionsApplicationTests.java
new file mode 100644
index 0000000..87e3085
--- /dev/null
+++ b/src/test/java/net/reformedwitness/cog/ConfessionsApplicationTests.java
@@ -0,0 +1,56 @@
+package net.reformedwitness.cog;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import net.reformedwitness.cog.repo.AuthorRepository;
+import net.reformedwitness.cog.repo.PostRepository;
+
+/**
+ * Full-context test against a real Postgres: proves the Flyway schema, the JPA mappings, and the markdown
+ * seeding (front-matter parsing + HTML rendering) all work end to end.
+ */
+@SpringBootTest(properties = {
+ "platform.storage.access-key=test",
+ "platform.storage.secret-key=test"
+})
+@Testcontainers
+class ConfessionsApplicationTests {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres =
+ new PostgreSQLContainer<>(DockerImageName.parse("postgres:18-alpine"));
+
+ @Autowired
+ PostRepository posts;
+
+ @Autowired
+ AuthorRepository authors;
+
+ @Test
+ void seedsPostsAndAuthorsFromBundledMarkdown() {
+ assertThat(posts.findByPublishedTrueOrderByPublishedOnDescIdDesc()).isNotEmpty();
+ assertThat(authors.findAllByOrderByNameAsc()).isNotEmpty();
+
+ var fourfold = posts.findBySlug("fourfold");
+ assertThat(fourfold).isPresent();
+ assertThat(fourfold.get().getTitle()).isNotBlank();
+ assertThat(fourfold.get().getAuthor()).isNotBlank();
+ assertThat(fourfold.get().getTags()).isNotEmpty();
+ assertThat(fourfold.get().getContentHtml()).contains("");
+ }
+
+ @Test
+ void tagCountsAreAggregated() {
+ assertThat(posts.tagCounts()).isNotEmpty();
+ }
+}
diff --git a/supabase/.gitignore b/supabase/.gitignore
deleted file mode 100644
index ad9264f..0000000
--- a/supabase/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Supabase
-.branches
-.temp
-
-# dotenvx
-.env.keys
-.env.local
-.env.*.local
diff --git a/supabase/config.toml b/supabase/config.toml
deleted file mode 100644
index 63d256e..0000000
--- a/supabase/config.toml
+++ /dev/null
@@ -1,388 +0,0 @@
-# For detailed configuration reference documentation, visit:
-# https://supabase.com/docs/guides/local-development/cli/config
-# A string used to distinguish different Supabase projects on the same host. Defaults to the
-# working directory name when running `supabase init`.
-project_id = "confessions-of-grace"
-
-[api]
-enabled = true
-# Port to use for the API URL.
-port = 54321
-# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
-# endpoints. `public` and `graphql_public` schemas are included by default.
-schemas = ["public", "graphql_public"]
-# Extra schemas to add to the search_path of every request.
-extra_search_path = ["public", "extensions"]
-# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
-# for accidental or malicious requests.
-max_rows = 1000
-
-[api.tls]
-# Enable HTTPS endpoints locally using a self-signed certificate.
-enabled = false
-# Paths to self-signed certificate pair.
-# cert_path = "../certs/my-cert.pem"
-# key_path = "../certs/my-key.pem"
-
-[db]
-# Port to use for the local database URL.
-port = 54322
-# Port used by db diff command to initialize the shadow database.
-shadow_port = 54320
-# Maximum amount of time to wait for health check when starting the local database.
-health_timeout = "2m"
-# The database major version to use. This has to be the same as your remote database's. Run `SHOW
-# server_version;` on the remote database to check.
-major_version = 17
-
-[db.pooler]
-enabled = false
-# Port to use for the local connection pooler.
-port = 54329
-# Specifies when a server connection can be reused by other clients.
-# Configure one of the supported pooler modes: `transaction`, `session`.
-pool_mode = "transaction"
-# How many server connections to allow per user/database pair.
-default_pool_size = 20
-# Maximum number of client connections allowed.
-max_client_conn = 100
-
-# [db.vault]
-# secret_key = "env(SECRET_VALUE)"
-
-[db.migrations]
-# If disabled, migrations will be skipped during a db push or reset.
-enabled = true
-# Specifies an ordered list of schema files that describe your database.
-# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
-schema_paths = []
-
-[db.seed]
-# If enabled, seeds the database after migrations during a db reset.
-enabled = true
-# Specifies an ordered list of seed files to load during db reset.
-# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
-sql_paths = ["./seed.sql"]
-
-[db.network_restrictions]
-# Enable management of network restrictions.
-enabled = false
-# List of IPv4 CIDR blocks allowed to connect to the database.
-# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
-allowed_cidrs = ["0.0.0.0/0"]
-# List of IPv6 CIDR blocks allowed to connect to the database.
-# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
-allowed_cidrs_v6 = ["::/0"]
-
-# Uncomment to reject non-secure connections to the database.
-# [db.ssl_enforcement]
-# enabled = true
-
-[realtime]
-enabled = true
-# Bind realtime via either IPv4 or IPv6. (default: IPv4)
-# ip_version = "IPv6"
-# The maximum length in bytes of HTTP request headers. (default: 4096)
-# max_header_length = 4096
-
-[studio]
-enabled = true
-# Port to use for Supabase Studio.
-port = 54323
-# External URL of the API server that frontend connects to.
-api_url = "http://127.0.0.1"
-# OpenAI API Key to use for Supabase AI in the Supabase Studio.
-openai_api_key = "env(OPENAI_API_KEY)"
-
-# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
-# are monitored, and you can view the emails that would have been sent from the web interface.
-[inbucket]
-enabled = true
-# Port to use for the email testing server web interface.
-port = 54324
-# Uncomment to expose additional ports for testing user applications that send emails.
-# smtp_port = 54325
-# pop3_port = 54326
-# admin_email = "admin@email.com"
-# sender_name = "Admin"
-
-[storage]
-enabled = true
-# The maximum file size allowed (e.g. "5MB", "500KB").
-file_size_limit = "50MiB"
-
-# Uncomment to configure local storage buckets
-# [storage.buckets.images]
-# public = false
-# file_size_limit = "50MiB"
-# allowed_mime_types = ["image/png", "image/jpeg"]
-# objects_path = "./images"
-
-# Allow connections via S3 compatible clients
-[storage.s3_protocol]
-enabled = true
-
-# Image transformation API is available to Supabase Pro plan.
-# [storage.image_transformation]
-# enabled = true
-
-# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
-# This feature is only available on the hosted platform.
-[storage.analytics]
-enabled = false
-max_namespaces = 5
-max_tables = 10
-max_catalogs = 2
-
-# Analytics Buckets is available to Supabase Pro plan.
-# [storage.analytics.buckets.my-warehouse]
-
-# Store vector embeddings in S3 for large and durable datasets
-# This feature is only available on the hosted platform.
-[storage.vector]
-enabled = false
-max_buckets = 10
-max_indexes = 5
-
-# Vector Buckets is available to Supabase Pro plan.
-# [storage.vector.buckets.documents-openai]
-
-[auth]
-enabled = true
-# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
-# in emails.
-site_url = "http://127.0.0.1:3000"
-# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
-additional_redirect_urls = ["https://127.0.0.1:3000"]
-# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
-jwt_expiry = 3600
-# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1).
-# jwt_issuer = ""
-# Path to JWT signing key. DO NOT commit your signing keys file to git.
-# signing_keys_path = "./signing_keys.json"
-# If disabled, the refresh token will never expire.
-enable_refresh_token_rotation = true
-# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
-# Requires enable_refresh_token_rotation = true.
-refresh_token_reuse_interval = 10
-# Allow/disallow new user signups to your project.
-enable_signup = true
-# Allow/disallow anonymous sign-ins to your project.
-enable_anonymous_sign_ins = false
-# Allow/disallow testing manual linking of accounts
-enable_manual_linking = false
-# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
-minimum_password_length = 6
-# Passwords that do not meet the following requirements will be rejected as weak. Supported values
-# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
-password_requirements = ""
-
-[auth.rate_limit]
-# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
-email_sent = 2
-# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
-sms_sent = 30
-# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
-anonymous_users = 30
-# Number of sessions that can be refreshed in a 5 minute interval per IP address.
-token_refresh = 150
-# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
-sign_in_sign_ups = 30
-# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
-token_verifications = 30
-# Number of Web3 logins that can be made in a 5 minute interval per IP address.
-web3 = 30
-
-# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
-# [auth.captcha]
-# enabled = true
-# provider = "hcaptcha"
-# secret = ""
-
-[auth.email]
-# Allow/disallow new user signups via email to your project.
-enable_signup = true
-# If enabled, a user will be required to confirm any email change on both the old, and new email
-# addresses. If disabled, only the new email is required to confirm.
-double_confirm_changes = true
-# If enabled, users need to confirm their email address before signing in.
-enable_confirmations = false
-# If enabled, users will need to reauthenticate or have logged in recently to change their password.
-secure_password_change = false
-# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
-max_frequency = "1s"
-# Number of characters used in the email OTP.
-otp_length = 6
-# Number of seconds before the email OTP expires (defaults to 1 hour).
-otp_expiry = 3600
-
-# Use a production-ready SMTP server
-# [auth.email.smtp]
-# enabled = true
-# host = "smtp.sendgrid.net"
-# port = 587
-# user = "apikey"
-# pass = "env(SENDGRID_API_KEY)"
-# admin_email = "admin@email.com"
-# sender_name = "Admin"
-
-# Uncomment to customize email template
-# [auth.email.template.invite]
-# subject = "You have been invited"
-# content_path = "./supabase/templates/invite.html"
-
-# Uncomment to customize notification email template
-# [auth.email.notification.password_changed]
-# enabled = true
-# subject = "Your password has been changed"
-# content_path = "./templates/password_changed_notification.html"
-
-[auth.sms]
-# Allow/disallow new user signups via SMS to your project.
-enable_signup = false
-# If enabled, users need to confirm their phone number before signing in.
-enable_confirmations = false
-# Template for sending OTP to users
-template = "Your code is {{ .Code }}"
-# Controls the minimum amount of time that must pass before sending another sms otp.
-max_frequency = "5s"
-
-# Use pre-defined map of phone number to OTP for testing.
-# [auth.sms.test_otp]
-# 4152127777 = "123456"
-
-# Configure logged in session timeouts.
-# [auth.sessions]
-# Force log out after the specified duration.
-# timebox = "24h"
-# Force log out if the user has been inactive longer than the specified duration.
-# inactivity_timeout = "8h"
-
-# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
-# [auth.hook.before_user_created]
-# enabled = true
-# uri = "pg-functions://postgres/auth/before-user-created-hook"
-
-# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
-# [auth.hook.custom_access_token]
-# enabled = true
-# uri = "pg-functions:////"
-
-# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
-[auth.sms.twilio]
-enabled = false
-account_sid = ""
-message_service_sid = ""
-# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
-auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
-
-# Multi-factor-authentication is available to Supabase Pro plan.
-[auth.mfa]
-# Control how many MFA factors can be enrolled at once per user.
-max_enrolled_factors = 10
-
-# Control MFA via App Authenticator (TOTP)
-[auth.mfa.totp]
-enroll_enabled = false
-verify_enabled = false
-
-# Configure MFA via Phone Messaging
-[auth.mfa.phone]
-enroll_enabled = false
-verify_enabled = false
-otp_length = 6
-template = "Your code is {{ .Code }}"
-max_frequency = "5s"
-
-# Configure MFA via WebAuthn
-# [auth.mfa.web_authn]
-# enroll_enabled = true
-# verify_enabled = true
-
-# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
-# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
-# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`.
-[auth.external.apple]
-enabled = false
-client_id = ""
-# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
-secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
-# Overrides the default auth redirectUrl.
-redirect_uri = ""
-# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
-# or any other third-party OIDC providers.
-url = ""
-# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
-skip_nonce_check = false
-# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
-email_optional = false
-
-# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
-# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
-[auth.web3.solana]
-enabled = false
-
-# Use Firebase Auth as a third-party provider alongside Supabase Auth.
-[auth.third_party.firebase]
-enabled = false
-# project_id = "my-firebase-project"
-
-# Use Auth0 as a third-party provider alongside Supabase Auth.
-[auth.third_party.auth0]
-enabled = false
-# tenant = "my-auth0-tenant"
-# tenant_region = "us"
-
-# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
-[auth.third_party.aws_cognito]
-enabled = false
-# user_pool_id = "my-user-pool-id"
-# user_pool_region = "us-east-1"
-
-# Use Clerk as a third-party provider alongside Supabase Auth.
-[auth.third_party.clerk]
-enabled = false
-# Obtain from https://clerk.com/setup/supabase
-# domain = "example.clerk.accounts.dev"
-
-# OAuth server configuration
-[auth.oauth_server]
-# Enable OAuth server functionality
-enabled = false
-# Path for OAuth consent flow UI
-authorization_url_path = "/oauth/consent"
-# Allow dynamic client registration
-allow_dynamic_registration = false
-
-[edge_runtime]
-enabled = true
-# Supported request policies: `oneshot`, `per_worker`.
-# `per_worker` (default) — enables hot reload during local development.
-# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
-policy = "per_worker"
-# Port to attach the Chrome inspector for debugging edge functions.
-inspector_port = 8083
-# The Deno major version to use.
-deno_version = 2
-
-# [edge_runtime.secrets]
-# secret_key = "env(SECRET_VALUE)"
-
-[analytics]
-enabled = true
-port = 54327
-# Configure one of the supported backends: `postgres`, `bigquery`.
-backend = "postgres"
-
-# Experimental features may be deprecated any time
-[experimental]
-# Configures Postgres storage engine to use OrioleDB (S3)
-orioledb_version = ""
-# Configures S3 bucket URL, eg. .s3-.amazonaws.com
-s3_host = "env(S3_HOST)"
-# Configures S3 bucket region, eg. us-east-1
-s3_region = "env(S3_REGION)"
-# Configures AWS_ACCESS_KEY_ID for S3 bucket
-s3_access_key = "env(S3_ACCESS_KEY)"
-# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
-s3_secret_key = "env(S3_SECRET_KEY)"
diff --git a/supabase/migrations/20240101000001_create_posts_table.sql b/supabase/migrations/20240101000001_create_posts_table.sql
deleted file mode 100644
index 8c872ee..0000000
--- a/supabase/migrations/20240101000001_create_posts_table.sql
+++ /dev/null
@@ -1,38 +0,0 @@
--- Create posts table
-create table if not exists public.posts (
- id text primary key, -- slug-based ID
- title text not null,
- date timestamptz not null,
- excerpt text not null default '',
- content text not null default '', -- raw markdown
- content_html text not null default '', -- pre-rendered HTML
- author text not null default '',
- tags text[] not null default '{}',
- cover_image text,
- published boolean not null default false,
- created_at timestamptz not null default now(),
- updated_at timestamptz not null default now()
-);
-
--- Indexes
-create index if not exists idx_posts_date on public.posts (date desc);
-create index if not exists idx_posts_published on public.posts (published);
-create index if not exists idx_posts_author on public.posts (author);
-create index if not exists idx_posts_tags on public.posts using gin (tags);
-
--- Auto-update trigger for updated_at
-create or replace function public.handle_updated_at()
-returns trigger as $$
-begin
- new.updated_at = now();
- return new;
-end;
-$$ language plpgsql;
-
-create trigger on_posts_updated
- before update on public.posts
- for each row
- execute function public.handle_updated_at();
-
--- Enable RLS
-alter table public.posts enable row level security;
diff --git a/supabase/migrations/20240101000002_create_admin_users_table.sql b/supabase/migrations/20240101000002_create_admin_users_table.sql
deleted file mode 100644
index 0f05837..0000000
--- a/supabase/migrations/20240101000002_create_admin_users_table.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Create admin_users table
-create table if not exists public.admin_users (
- id uuid primary key default gen_random_uuid(),
- user_id uuid not null references auth.users(id) on delete cascade,
- email text not null,
- role text not null default 'editor' check (role in ('super_admin', 'admin', 'editor')),
- created_at timestamptz not null default now(),
- unique(user_id)
-);
-
--- Index on user_id for fast lookups
-create index if not exists idx_admin_users_user_id on public.admin_users (user_id);
-
--- Enable RLS
-alter table public.admin_users enable row level security;
diff --git a/supabase/migrations/20240101000003_create_existing_tables.sql b/supabase/migrations/20240101000003_create_existing_tables.sql
deleted file mode 100644
index a9363ad..0000000
--- a/supabase/migrations/20240101000003_create_existing_tables.sql
+++ /dev/null
@@ -1,35 +0,0 @@
--- Create tables that were originally created via Supabase dashboard.
--- Using IF NOT EXISTS so this is safe to run on existing databases.
-
--- Comments table
-create table if not exists public.comments (
- id bigint generated always as identity primary key,
- name text not null,
- email text not null,
- comment text not null,
- post_id text not null,
- created_at timestamptz not null default now()
-);
-
-alter table public.comments enable row level security;
-
--- Subscriptions table
-create table if not exists public.subscriptions (
- id bigint generated always as identity primary key,
- email text not null unique,
- created_at timestamptz not null default now()
-);
-
-alter table public.subscriptions enable row level security;
-
--- Authors table
-create table if not exists public.authors (
- name text primary key,
- bio text not null default '',
- x_link text,
- fb_link text,
- insta_link text,
- pfp_link text
-);
-
-alter table public.authors enable row level security;
diff --git a/supabase/migrations/20240101000004_rls_policies.sql b/supabase/migrations/20240101000004_rls_policies.sql
deleted file mode 100644
index 103d4a6..0000000
--- a/supabase/migrations/20240101000004_rls_policies.sql
+++ /dev/null
@@ -1,151 +0,0 @@
--- Helper function: check if current user is an admin
-create or replace function public.is_admin()
-returns boolean as $$
-begin
- return exists (
- select 1 from public.admin_users
- where user_id = auth.uid()
- );
-end;
-$$ language plpgsql security definer;
-
--- Helper function: check if current user has a specific role or higher
-create or replace function public.has_admin_role(required_role text)
-returns boolean as $$
-declare
- user_role text;
-begin
- select role into user_role from public.admin_users
- where user_id = auth.uid();
-
- if user_role is null then
- return false;
- end if;
-
- -- Role hierarchy: super_admin > admin > editor
- if required_role = 'editor' then
- return user_role in ('editor', 'admin', 'super_admin');
- elsif required_role = 'admin' then
- return user_role in ('admin', 'super_admin');
- elsif required_role = 'super_admin' then
- return user_role = 'super_admin';
- end if;
-
- return false;
-end;
-$$ language plpgsql security definer;
-
--- ============================================
--- POSTS policies (drop first in case of partial previous run)
--- ============================================
-drop policy if exists "Public can read published posts" on public.posts;
-drop policy if exists "Editors can insert posts" on public.posts;
-drop policy if exists "Editors can update posts" on public.posts;
-drop policy if exists "Admins can delete posts" on public.posts;
-
-create policy "Public can read published posts"
- on public.posts for select
- using (published = true or public.is_admin());
-
-create policy "Editors can insert posts"
- on public.posts for insert
- with check (public.has_admin_role('editor'));
-
-create policy "Editors can update posts"
- on public.posts for update
- using (public.has_admin_role('editor'));
-
-create policy "Admins can delete posts"
- on public.posts for delete
- using (public.has_admin_role('admin'));
-
--- ============================================
--- COMMENTS policies
--- ============================================
-drop policy if exists "Public can read comments" on public.comments;
-drop policy if exists "Public can insert comments" on public.comments;
-drop policy if exists "Editors can delete comments" on public.comments;
-drop policy if exists "Editors can update comments" on public.comments;
-
-create policy "Public can read comments"
- on public.comments for select
- using (true);
-
-create policy "Public can insert comments"
- on public.comments for insert
- with check (true);
-
-create policy "Editors can delete comments"
- on public.comments for delete
- using (public.has_admin_role('editor'));
-
-create policy "Editors can update comments"
- on public.comments for update
- using (public.has_admin_role('editor'));
-
--- ============================================
--- SUBSCRIPTIONS policies
--- ============================================
-drop policy if exists "Public can insert subscriptions" on public.subscriptions;
-drop policy if exists "Public can read subscriptions" on public.subscriptions;
-drop policy if exists "Admins can delete subscriptions" on public.subscriptions;
-
-create policy "Public can insert subscriptions"
- on public.subscriptions for insert
- with check (true);
-
-create policy "Public can read subscriptions"
- on public.subscriptions for select
- using (true);
-
-create policy "Admins can delete subscriptions"
- on public.subscriptions for delete
- using (public.has_admin_role('admin'));
-
--- ============================================
--- AUTHORS policies
--- ============================================
-drop policy if exists "Public can read authors" on public.authors;
-drop policy if exists "Admins can insert authors" on public.authors;
-drop policy if exists "Admins can update authors" on public.authors;
-drop policy if exists "Super admins can delete authors" on public.authors;
-
-create policy "Public can read authors"
- on public.authors for select
- using (true);
-
-create policy "Admins can insert authors"
- on public.authors for insert
- with check (public.has_admin_role('admin'));
-
-create policy "Admins can update authors"
- on public.authors for update
- using (public.has_admin_role('admin'));
-
-create policy "Super admins can delete authors"
- on public.authors for delete
- using (public.has_admin_role('super_admin'));
-
--- ============================================
--- ADMIN_USERS policies
--- ============================================
-drop policy if exists "Admins can read admin users" on public.admin_users;
-drop policy if exists "Super admins can insert admin users" on public.admin_users;
-drop policy if exists "Super admins can update admin users" on public.admin_users;
-drop policy if exists "Super admins can delete admin users" on public.admin_users;
-
-create policy "Admins can read admin users"
- on public.admin_users for select
- using (public.is_admin());
-
-create policy "Super admins can insert admin users"
- on public.admin_users for insert
- with check (public.has_admin_role('super_admin'));
-
-create policy "Super admins can update admin users"
- on public.admin_users for update
- using (public.has_admin_role('super_admin'));
-
-create policy "Super admins can delete admin users"
- on public.admin_users for delete
- using (public.has_admin_role('super_admin'));
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index e7ff3a2..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": [
- "./*"
- ]
- }
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts"
- ],
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/types/index.ts b/types/index.ts
deleted file mode 100644
index 21719fa..0000000
--- a/types/index.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-export interface PostData {
- id: string;
- title: string;
- date: string;
- excerpt: string;
- content: string;
- author: string;
- tags: string[];
- coverImage?: string;
-}
-
-export interface PostMetadata {
- id: string;
- title: string;
- date: string;
- excerpt: string;
- author: string;
- tags: string[];
- coverImage?: string;
-}
-
-export interface PostFormData {
- id: string;
- title: string;
- date: string;
- excerpt: string;
- content: string;
- author: string;
- tags: string[];
- coverImage?: string;
- published: boolean;
-}
-
-export interface AdminUser {
- id: string;
- user_id: string;
- email: string;
- role: "super_admin" | "admin" | "editor";
- created_at: string;
-}
-
-export interface Comment {
- id: number;
- name: string;
- email: string;
- comment: string;
- post_id: string;
- created_at: string;
-}
-
-export interface Subscription {
- id: number;
- email: string;
- created_at: string;
-}
-
-export interface Author {
- name: string;
- bio: string;
- x_link?: string;
- fb_link?: string;
- insta_link?: string;
- pfp_link?: string;
-}