diff --git a/.gitignore b/.gitignore index 475a2b5d..2f69a856 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +docker/.env # dependencies /node_modules diff --git a/Dockerfile b/Dockerfile index f8d799af..0a899b26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,40 @@ -FROM node:22-slim - -ENV PNPM_HOME="/root/.local/share/pnpm" -ENV PATH="${PATH}:${PNPM_HOME}" - -RUN npm install --global pnpm - -COPY . /app - +FROM docker.io/node:22-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +RUN corepack prepare pnpm@10.29.3 --activate WORKDIR /app +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/readest-app/package.json ./apps/readest-app/ +COPY patches/ ./patches/ +COPY packages/ ./packages/ -RUN pnpm install - +FROM base AS dependencies +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN pnpm --filter @readest/readest-app setup-vendors +FROM dependencies AS development-stage +COPY . . WORKDIR /app/apps/readest-app +EXPOSE 3000 +ENTRYPOINT ["pnpm", "dev-web", "-H", "0.0.0.0"] +FROM base AS build +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ARG NEXT_PUBLIC_APP_PLATFORM +ARG NEXT_PUBLIC_API_BASE_URL +ARG NEXT_PUBLIC_OBJECT_STORAGE_TYPE +ARG NEXT_PUBLIC_STORAGE_FIXED_QUOTA +ARG NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA +COPY --from=dependencies /app/node_modules /app/node_modules +COPY --from=dependencies /app/apps/readest-app/node_modules /app/apps/readest-app/node_modules +COPY --from=dependencies /app/apps/readest-app/public/vendor /app/apps/readest-app/public/vendor +COPY --from=dependencies /app/packages/foliate-js/node_modules /app/packages/foliate-js/node_modules +COPY . . +WORKDIR /app/apps/readest-app RUN pnpm build-web -ENTRYPOINT ["pnpm", "start-web"] +FROM build as production-stage +ENTRYPOINT ["pnpm", "start-web", "-H", "0.0.0.0"] +EXPOSE 3000 diff --git a/apps/readest-app/src/utils/supabase.ts b/apps/readest-app/src/utils/supabase.ts index 744905c7..5b947704 100644 --- a/apps/readest-app/src/utils/supabase.ts +++ b/apps/readest-app/src/utils/supabase.ts @@ -1,9 +1,11 @@ import { createClient } from '@supabase/supabase-js'; const supabaseUrl = + process.env['SUPABASE_URL'] || process.env['NEXT_PUBLIC_SUPABASE_URL'] || atob(process.env['NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64']!); const supabaseAnonKey = + process.env['SUPABASE_ANON_KEY'] || process.env['NEXT_PUBLIC_SUPABASE_ANON_KEY'] || atob(process.env['NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64']!); diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..4678e27e --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,51 @@ +# ip used by frontend/client-side urls to access the backend +HOST_IP=localhost + +# db(psql) config +# change to strong password, min 32 chars +POSTGRES_PASSWORD=your-super-secret-postgres-password +POSTGRES_HOST=db +POSTGRES_PORT=5432 +POSTGRES_DB=postgres + +# jwt config +JWT_EXPIRY=3600 +JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters +# both ANON_KEY and SERVICE_ROLE_KEY should be generated using the above set secret +# sign this payload -> {"role": "anon"} <- for ANON_KEY with JWT_SECRET +ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiJ9.HmbvN-NgnsK1Lbk5e8Dti9SS6SFB384DsIT8QVLwUo8 +# sign this payload -> {"role": "service_role"} <- for SERVICE_ROLE_KEY with JWT_SECRET +SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.ym708-_0SlTjvc0pAK56bXQK2srKAo8RbJfA3czgkFo + +KONG_HTTP_PORT=8000 + +# auth (gotrue) +API_EXTERNAL_URL=http://localhost:8000 +SITE_URL=http://localhost:3000 +ADDITIONAL_REDIRECT_URLS=http://localhost:3000/**,http://localhost:8000/** + +# readest auth stuff +DISABLE_SIGNUP=false +ENABLE_EMAIL_SIGNUP=true +ENABLE_EMAIL_AUTOCONFIRM=true +ENABLE_ANONYMOUS_USERS=false + +# only needed if ENABLE_EMAIL_AUTOCONFIRM is false +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_SENDER_NAME=Readest + +# PostgREST shiz +PGRST_DB_SCHEMAS=public,graphql_public + +# s3 storage through MinIO +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin-secret-password +S3_BUCKET_NAME=readest-files + +# 1GB fixed quota for storage and 50k for translations +STORAGE_FIXED_QUOTA=1073741824 +TRANSLATION_FIXED_QUOTA=50000 \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..d81a1e13 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,122 @@ +# Self-Hosting with Docker/Podman with Compose + +## Stack + +| service | Image | Description | +| --------------- | --------------------------- | ------------------------------------------------- | +| **client** | from `../Dockerfile` | readest frontend | +| **db** | `supabase/postgres` | psql db with supabase extensions | +| **kong** | `kong:2.8.1` | api gateway routing requests to supabase services | +| **auth** | `supabase/gotrue:v2.185.0` | auth service (email, JWT) | +| **rest** | `postgrest/postgrest:v14.3` | psql rest api | +| **minio** | `minio/minio` | s3 storage | +| **minio-setup** | `minio/mc` | helper container to create s3 buckets | + +### Exposed ports + +| Port | Service | +| ------ | ---------------- | +| `3000` | readest | +| `7000` | kong API gateway | +| `9000` | MinIO S3 API | +| `9001` | MinIO console UI | + +--- + +## Running with Docker/Podman Compose + +### 1. setup .env + +```bash +cp docker/.env.example docker/.env +``` + +update `docker/.env`: + +- update `POSTGRES_PASSWORD` to a strong password (32+ chars) +- update `JWT_SECRET` to a random secret (32+ chars) +- regenerate `ANON_KEY` and `SERVICE_ROLE_KEY` as HS256 JWTs signed with your `JWT_SECRET` (use [jwt.io](https://jwt.io/) or a similar tool): + - `ANON_KEY` payload: `{"role": "anon"}` + - `SERVICE_ROLE_KEY` payload: `{"role": "service_role"}` +- set `MINIO_ROOT_PASSWORD` to a strong password + +### 2. Start the Stack + +run from the `docker/` directory: + +```bash +cd docker +docker compose up --build -d +``` + +the client image is built locally on first run. subsequent starts reuse the cached image. + +### 3. Access + +- Readest app: `http://localhost:3000` +- MinIO console: `http://localhost:9001` (login with `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`) + +### Hot Reload (development) + +to develop using the compose stack, set the build target on `client` to `development-stage`, which'll runs the next.js dev server. to enable hot reload, uncomment the `volumes` block in the `client` service in `compose.yaml`: + +```yaml +volumes: + - ../:/app + - /app/node_modules + - /app/apps/readest-app/node_modules + - /app/apps/readest-app/public/vendor + - /app/apps/readest-app/.next + - /app/packages/foliate-js/node_modules +``` + +the first mount overlays your local repo into the container. the remaining anonymous volumes shadow the directories that were pre-built inside the image, so the container's installed deps and vendor assets are used instead of what's on your host. + +### Stop the Stack + +```bash +cd docker +docker compose down +``` + +to also remove volumes (database and storage data): + +```bash +cd docker +docker compose down -v +``` + +--- + +## Building the Dockerfile standalone + +the `Dockerfile` requires Build args for the next.js public env vars (they are inlined at build time) + +```bash +docker build \ + --target production-stage \ + --build-arg NEXT_PUBLIC_SUPABASE_URL=http://localhost:7000 \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY= \ + --build-arg NEXT_PUBLIC_APP_PLATFORM=web \ + --build-arg NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 \ + --build-arg NEXT_PUBLIC_OBJECT_STORAGE_TYPE=s3 \ + --build-arg NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824 \ + --build-arg NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA=50000 \ + -t readest-client \ + . +``` + +run the built image: + +```bash +docker run -p 3000:3000 \ + -e SUPABASE_URL=http://kong:8000 \ + -e SUPABASE_ANON_KEY= \ + -e SUPABASE_ADMIN_KEY= \ + -e S3_ENDPOINT=http://localhost:9000 \ + -e S3_REGION=us-east-1 \ + -e S3_BUCKET_NAME=readest-files \ + -e S3_ACCESS_KEY_ID= \ + -e S3_SECRET_ACCESS_KEY= \ + readest-client +``` diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 00000000..c360a306 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,160 @@ +name: readest + +services: + db: + container_name: supabase-db + image: supabase/postgres:15.8.1.085 + restart: unless-stopped + volumes: + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + - ./volumes/db/init/schema.sql:/docker-entrypoint-initdb.d/init-scripts/100-schema.sql:Z + - db-data:/var/lib/postgresql/data:Z + - db-config:/etc/postgresql-custom + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + ports: + - "${KONG_HTTP_PORT}:8000/tcp" + volumes: + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.185.0 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB} + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v14.3 + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: ["postgrest"] + + minio: + container_name: readest-minio + image: minio/minio + restart: unless-stopped + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + command: server --console-address ":9001" /data + volumes: + - minio-data:/data:z + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + + minio-setup: + image: minio/mc + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} && + mc mb --ignore-existing myminio/${S3_BUCKET_NAME} + " + + client: + container_name: readest-client + build: + context: .. + target: production-stage + args: + NEXT_PUBLIC_SUPABASE_URL: http://${HOST_IP}:7000 + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY} + NEXT_PUBLIC_APP_PLATFORM: web + NEXT_PUBLIC_API_BASE_URL: http://${HOST_IP}:3000 + NEXT_PUBLIC_OBJECT_STORAGE_TYPE: s3 + NEXT_PUBLIC_STORAGE_FIXED_QUOTA: ${STORAGE_FIXED_QUOTA} + NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA: ${TRANSLATION_FIXED_QUOTA} + restart: unless-stopped + ports: + - "3000:3000" +# to enable hot reload, mount the source code as a volume, and artifacts as anon volumes +# volumes: +# - ../:/app +# - /app/node_modules +# - /app/apps/readest-app/node_modules +# - /app/apps/readest-app/public/vendor +# - /app/apps/readest-app/.next +# - /app/packages/foliate-js/node_modules + environment: + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_ADMIN_KEY: ${SERVICE_ROLE_KEY} + S3_ENDPOINT: http://${HOST_IP}:9000 + S3_REGION: us-east-1 + S3_BUCKET_NAME: ${S3_BUCKET_NAME} + S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER} + S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} + depends_on: + - kong + - minio + +volumes: + db-config: + db-data: + minio-data: \ No newline at end of file diff --git a/docker/volumes/api/kong.yml b/docker/volumes/api/kong.yml new file mode 100644 index 00000000..ceee562e --- /dev/null +++ b/docker/volumes/api/kong.yml @@ -0,0 +1,95 @@ +_format_version: '2.1' +_transform: true + +consumers: + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +services: + - name: auth-v1-health + url: http://auth:9999/health + routes: + - name: auth-v1-health + strip_path: true + paths: + - /auth/v1/health + plugins: + - name: cors + + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + - name: auth-v1 + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + - name: rest-v1 + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon diff --git a/docker/volumes/db/init/schema.sql b/docker/volumes/db/init/schema.sql new file mode 100644 index 00000000..58890517 --- /dev/null +++ b/docker/volumes/db/init/schema.sql @@ -0,0 +1,105 @@ +ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin; +ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin; + +CREATE TABLE public.books ( + user_id uuid NOT NULL, + book_hash text NOT NULL, + meta_hash text NULL, + format text NULL, + title text NULL, + source_title text NULL, + author text NULL, + "group" text NULL, + tags text[] NULL, + created_at timestamp with time zone NULL DEFAULT now(), + updated_at timestamp with time zone NULL DEFAULT now(), + deleted_at timestamp with time zone NULL, + uploaded_at timestamp with time zone NULL, + progress integer[] NULL, + reading_status text NULL, + group_id text NULL, + group_name text NULL, + metadata json NULL, + CONSTRAINT books_pkey PRIMARY KEY (user_id, book_hash), + CONSTRAINT books_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE +); + +ALTER TABLE public.books ENABLE ROW LEVEL SECURITY; +CREATE POLICY select_books ON public.books FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY insert_books ON public.books FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id); +CREATE POLICY update_books ON public.books FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY delete_books ON public.books FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id); + +CREATE TABLE public.book_configs ( + user_id uuid NOT NULL, + book_hash text NOT NULL, + meta_hash text NULL, + location text NULL, + xpointer text NULL, + progress jsonb NULL, + search_config jsonb NULL, + view_settings jsonb NULL, + created_at timestamp with time zone NULL DEFAULT now(), + updated_at timestamp with time zone NULL DEFAULT now(), + deleted_at timestamp with time zone NULL, + CONSTRAINT book_configs_pkey PRIMARY KEY (user_id, book_hash), + CONSTRAINT book_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE +); + +ALTER TABLE public.book_configs ENABLE ROW LEVEL SECURITY; +CREATE POLICY select_book_configs ON public.book_configs FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY insert_book_configs ON public.book_configs FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id); +CREATE POLICY update_book_configs ON public.book_configs FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY delete_book_configs ON public.book_configs FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id); + +CREATE TABLE public.book_notes ( + user_id uuid NOT NULL, + book_hash text NOT NULL, + meta_hash text NULL, + id text NOT NULL, + type text NULL, + cfi text NULL, + text text NULL, + style text NULL, + color text NULL, + note text NULL, + created_at timestamp with time zone NULL DEFAULT now(), + updated_at timestamp with time zone NULL DEFAULT now(), + deleted_at timestamp with time zone NULL, + CONSTRAINT book_notes_pkey PRIMARY KEY (user_id, book_hash, id), + CONSTRAINT book_notes_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE +); + +ALTER TABLE public.book_notes ENABLE ROW LEVEL SECURITY; +CREATE POLICY select_book_notes ON public.book_notes FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY insert_book_notes ON public.book_notes FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id); +CREATE POLICY update_book_notes ON public.book_notes FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY delete_book_notes ON public.book_notes FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id); + +CREATE TABLE public.files ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + user_id uuid NOT NULL, + book_hash text NULL, + file_key text NOT NULL, + file_size bigint NOT NULL, + created_at timestamp with time zone NULL DEFAULT now(), + deleted_at timestamp with time zone NULL, + CONSTRAINT files_pkey PRIMARY KEY (id), + CONSTRAINT files_file_key_key UNIQUE (file_key), + CONSTRAINT files_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE +); + +CREATE INDEX idx_files_user_id_deleted_at ON public.files (user_id, deleted_at); +CREATE INDEX idx_files_file_key ON public.files (file_key); +CREATE INDEX idx_files_file_key_deleted_at ON public.files (file_key, deleted_at); + +ALTER TABLE public.files ENABLE ROW LEVEL SECURITY; +CREATE POLICY files_insert ON public.files FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY files_select ON public.files FOR SELECT USING (auth.uid() = user_id AND deleted_at IS NULL); +CREATE POLICY files_update ON public.files FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (deleted_at IS NULL OR deleted_at > now()); +CREATE POLICY files_delete ON public.files FOR DELETE USING (auth.uid() = user_id); + +GRANT ALL ON public.books TO authenticated; +GRANT ALL ON public.book_configs TO authenticated; +GRANT ALL ON public.book_notes TO authenticated; +GRANT ALL ON public.files TO authenticated; \ No newline at end of file diff --git a/docker/volumes/db/jwt.sql b/docker/volumes/db/jwt.sql new file mode 100644 index 00000000..cfd3b160 --- /dev/null +++ b/docker/volumes/db/jwt.sql @@ -0,0 +1,5 @@ +\set jwt_secret `echo "$JWT_SECRET"` +\set jwt_exp `echo "$JWT_EXP"` + +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; diff --git a/docker/volumes/db/roles.sql b/docker/volumes/db/roles.sql new file mode 100644 index 00000000..a592043e --- /dev/null +++ b/docker/volumes/db/roles.sql @@ -0,0 +1,7 @@ +-- NOTE: change to your own passwords for production environments +\set pgpass `echo "$POSTGRES_PASSWORD"` + +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';