From 177705cfb1bcf61f93d674c2240b97b6a5b43ec4 Mon Sep 17 00:00:00 2001 From: Gib Date: Fri, 20 Jun 2025 17:01:22 -0500 Subject: [PATCH] Going from down to up, we are stopping at prettierrc as far as making sure we have everything configured. --- .env.example | 34 + .gitignore | 46 ++ .prettierrc | 6 + README.md | 124 ++++ bun.lockb | Bin 0 -> 325964 bytes components.json | 21 + drizzle.config.ts | 11 + eslint.config.js | 66 ++ next.config.js | 70 +++ package.json | 87 +++ postcss.config.js | 5 + prettier.config.js | 4 + public/favicon.ico | Bin 0 -> 23600 bytes scripts/files_to_clipboard | 99 +++ scripts/next/config/next.config.build.js | 76 +++ scripts/next/config/next.config.default.js | 70 +++ scripts/next/docker/Dockerfile | 60 ++ scripts/next/docker/compose.yaml | 16 + scripts/next/update_container | 8 + scripts/supabase/db/schema.sql | 126 ++++ scripts/supabase/docker/.env.example | 158 +++++ .../supabase/docker/docker-compose.dev.yml | 41 ++ scripts/supabase/docker/docker-compose.s3.yml | 105 ++++ scripts/supabase/docker/docker-compose.yml | 579 ++++++++++++++++++ scripts/supabase/docker/volumes/api/kong.yml | 241 ++++++++ .../supabase/docker/volumes/db/_supabase.sql | 3 + scripts/supabase/docker/volumes/db/jwt.sql | 5 + scripts/supabase/docker/volumes/db/logs.sql | 6 + scripts/supabase/docker/volumes/db/pooler.sql | 6 + .../supabase/docker/volumes/db/realtime.sql | 4 + scripts/supabase/docker/volumes/db/roles.sql | 8 + .../supabase/docker/volumes/db/webhooks.sql | 208 +++++++ .../docker/volumes/functions/hello/index.ts | 15 + .../docker/volumes/functions/main/index.ts | 94 +++ .../supabase/docker/volumes/logs/vector.yml | 232 +++++++ .../supabase/docker/volumes/pooler/pooler.exs | 30 + scripts/supabase/generate_types | 133 ++++ .../mail_templates/change_email_address.html | 43 ++ .../mail_templates/confirm_signup.html | 41 ++ .../supabase/mail_templates/invite_user.html | 41 ++ .../supabase/mail_templates/magic_link.html | 43 ++ .../mail_templates/reauthentication.html | 42 ++ .../mail_templates/reset_password.html | 43 ++ sentry.server.config.ts | 9 + src/app/layout.tsx | 25 + src/app/page.tsx | 37 ++ src/components/ui/avatar.tsx | 53 ++ src/components/ui/badge.tsx | 46 ++ src/components/ui/based-avatar.tsx | 57 ++ src/components/ui/button.tsx | 62 ++ src/components/ui/calendar.tsx | 210 +++++++ src/components/ui/card.tsx | 92 +++ src/components/ui/checkbox.tsx | 32 + src/components/ui/combobox.tsx | 96 +++ src/components/ui/command.tsx | 184 ++++++ src/components/ui/dialog.tsx | 143 +++++ src/components/ui/drawer.tsx | 135 ++++ src/components/ui/dropdown-menu.tsx | 257 ++++++++ src/components/ui/form.tsx | 168 +++++ src/components/ui/label.tsx | 24 + src/components/ui/loading.tsx | 41 ++ src/components/ui/popover.tsx | 48 ++ src/components/ui/progress.tsx | 31 + src/components/ui/table.tsx | 116 ++++ src/env.js | 73 +++ src/lib/utils.ts | 6 + src/server/db/index.ts | 22 + src/server/db/schema.ts | 21 + src/styles/globals.css | 125 ++++ tsconfig.json | 42 ++ 70 files changed, 5205 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 components.json create mode 100644 drizzle.config.ts create mode 100644 eslint.config.js create mode 100644 next.config.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 public/favicon.ico create mode 100755 scripts/files_to_clipboard create mode 100644 scripts/next/config/next.config.build.js create mode 100644 scripts/next/config/next.config.default.js create mode 100644 scripts/next/docker/Dockerfile create mode 100644 scripts/next/docker/compose.yaml create mode 100755 scripts/next/update_container create mode 100644 scripts/supabase/db/schema.sql create mode 100644 scripts/supabase/docker/.env.example create mode 100644 scripts/supabase/docker/docker-compose.dev.yml create mode 100644 scripts/supabase/docker/docker-compose.s3.yml create mode 100644 scripts/supabase/docker/docker-compose.yml create mode 100644 scripts/supabase/docker/volumes/api/kong.yml create mode 100644 scripts/supabase/docker/volumes/db/_supabase.sql create mode 100644 scripts/supabase/docker/volumes/db/jwt.sql create mode 100644 scripts/supabase/docker/volumes/db/logs.sql create mode 100644 scripts/supabase/docker/volumes/db/pooler.sql create mode 100644 scripts/supabase/docker/volumes/db/realtime.sql create mode 100644 scripts/supabase/docker/volumes/db/roles.sql create mode 100644 scripts/supabase/docker/volumes/db/webhooks.sql create mode 100644 scripts/supabase/docker/volumes/functions/hello/index.ts create mode 100644 scripts/supabase/docker/volumes/functions/main/index.ts create mode 100644 scripts/supabase/docker/volumes/logs/vector.yml create mode 100644 scripts/supabase/docker/volumes/pooler/pooler.exs create mode 100755 scripts/supabase/generate_types create mode 100644 scripts/supabase/mail_templates/change_email_address.html create mode 100644 scripts/supabase/mail_templates/confirm_signup.html create mode 100644 scripts/supabase/mail_templates/invite_user.html create mode 100644 scripts/supabase/mail_templates/magic_link.html create mode 100644 scripts/supabase/mail_templates/reauthentication.html create mode 100644 scripts/supabase/mail_templates/reset_password.html create mode 100644 sentry.server.config.ts create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/based-avatar.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/combobox.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/loading.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/env.js create mode 100644 src/lib/utils.ts create mode 100644 src/server/db/index.ts create mode 100644 src/server/db/schema.ts create mode 100644 src/styles/globals.css create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8176828 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# When adding additional environment variables, the schema in "/src/env.js" +# should be updated accordingly. + +# Example: +# SERVERVAR="foo" +# NEXT_PUBLIC_CLIENTVAR="bar" + +### Server Variables ### +# Next Variables # Default Values: +NODE_ENV= # development +SKIP_ENV_VALIDATION= # false +# Sentry Variables # Default Values: +SENTRY_AUTH_TOKEN= +CI= # true + +### Client Variables ### +# Next Variables # Default Values: +NEXT_PUBLIC_SITE_URL= # http://localhost:3000 +# Supabase Variables +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= +# Sentry Variables # Default Values +NEXT_PUBLIC_SENTRY_DSN= +NEXT_PUBLIC_SENTRY_URL= # https://sentry.gbrown.org +NEXT_PUBLIC_SENTRY_ORG= # gib +NEXT_PUBLIC_SENTRY_PROJECT_NAME= + +# Drizzle & Supabase CLI Variables + # Default Values: +DB_USER= # postgres +DB_PASSWORD= +DB_HOST= # localhost +DB_PORT= # 5432 +DB_NAME= # postgres diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c24a835 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# database +/prisma/db.sqlite +/prisma/db.sqlite-journal +db.sqlite + +# next.js +/.next/ +/out/ +next-env.d.ts + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# idea files +.idea \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..75ebcfc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all", + "tabWidth": 2 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f8c2c9 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +

+
+ Next Template +
+ Next Template +
+

+ +
+ +

How to run:

+
+ +### Clone the Repository & Install Dependencies + +```bash +git clone https://git.gbrown.org/gib/next-template.git +``` + +```bash +cd next-template +``` + +I would recommend using [bun](https://bun.sh/) to install dependencies. + +```bash +bun -i +``` + +You will also need docker installed on whatever host you plan to run the Supabase instance from, whether locally, or on a home server or a VPS or whatever. Or you can just use the Supabase SaaS if you want to have a much easier time, probably. I wouldn't know! + +### Add your environment variables + +Copy the example environment variable files and paste them in the same directory named `.env`. + +```bash +cp ./.env.example ./.env +``` + +```bash +cp ./scripts/supabase/docker/.env.example ./scripts/supabase/docker/.env +``` + +Add your secrets to the `.env` files you just copied. + +### Host Supabase Locally + + - Follow the instructions [here](https://supabase.com/docs/guides/self-hosting/docker) to host Supabase with Docker. + - You will need to make sure you have some way to connect to the postgres database from the host. I had to remove the database port from the supabase-pooler and add it to the supabase-db in order to directly connect to it. This will be important for generating our types. This is not strictly necessary, and honestly I think I may even just have the docker compose set up to do this already, as I can't figure out why I would want to port to the spooler open on my host anyways. + +### Create your database schema & generate your types. + +- Copy the contents of the schema file located in `./scripts/supabase/db/schema.sql` & paste it into the SQL editor on the Web UI of your Supabase instance. Run the SQL. There should be no errors & you should now be able to see the profiles & statuses tables in the table editor. +```bash +cat ./src/server/db/schema.sql | wl-copy # If you are on Linux (& using wayland & have wl-copy installed) +``` + +- Generate your types. + - This can be a bit weird depending on what your setup is. If you are running Supabase locally on the same host that you are running your dev server, then this should be straightforward. If you are using the Supabase SaaS, then this is even more straightforward. If you are like me, and you are connecting to a self hosted instance of Supabase on your home server while developing, then you must clone this reposity on your server so that the command line tool can generate the types from your open postgres port on your Host, which is why the docker compose is configured how it was & why I mentioned this earlier. + +You will need to run the supabase cli tool with sudo in my experience. What I would recommend to you is to run the command + +```bash +sudo npx supabase --help +``` + +You will be prompted to install the supabase cli tool if you do not already have it installed, which you probably don't since root is running this. After that, you can run the following command below, replacing the password and the port to match your own Supabase Postgres Database port & password. + +```bash +sudo npx supabase gen types typescript \ +--db-url "postgres://postgres:password@localhost:5432/postgres" \ +--schema public \ +> ./src/utils/supabase/types.ts +``` + +There is also a script in the `scripts` folder called `generate_types` which *should* do this for you. + +```bash +./scripts/generate_types +``` + +### Start your development environment. + +Run + + ```bash +bun dev + ``` + +to start your development environment with turbopack + +You can also run + +```bash +bun dev:slow +``` + +to start your development environment with webpack + +### Start your Production Environment. + +There are Dockerfiles & docker compose files that can be found in the `./scripts/docker` folder for the Next.js website. There is also a script called `reload_container` located in the `./scripts/` folder which was created to quickly update the container, but this will give you a better idea of what you need to do. First, build the image with + +```bash +sudo docker compose -f ./scripts/next/docker/compose.yml build +``` + +then you can run the container with + +```bash +sudo docker compose -f ./scripts/next/docker/compose up -d +``` + +Now, you may end up with some build errors. The `reload_containers` script swaps out the next config before it runs the docker build to skip any build errors, so you may want to do this as well, though you are welcome to fix the build errors as well, of course! + +### Fin + +I am sure I am missing a lot of stuff so feel free to open an issue if you have any questions or if you feel that I should add something here! + +
diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..5ed3a697d3a3d86ceacc797e84bf786167a34f0f GIT binary patch literal 325964 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 literal 0 HcmV?d00001 diff --git a/scripts/files_to_clipboard b/scripts/files_to_clipboard new file mode 100755 index 0000000..9be0ff2 --- /dev/null +++ b/scripts/files_to_clipboard @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import os +import sys +import argparse +from pathlib import Path +import pyperclip +import questionary + +# List of directories to exclude +EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'} + +def collect_files(project_path): + """ + Collects files from the project directory, excluding specified directories and filtering by extensions. + Returns a list of file paths relative to the project directory. + """ + collected_files = [] + + for root, dirs, files in os.walk(project_path): + # Exclude specified directories + dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS] + + for file in files: + file_path = Path(root) / file + relative_path = file_path.relative_to(project_path) + collected_files.append(relative_path) + + return collected_files + +def main(): + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Generate Markdown from selected files.') + parser.add_argument('path', nargs='?', default='.', help='Path to the project directory') + args = parser.parse_args() + + project_path = Path(args.path).resolve() + if not project_path.is_dir(): + print(f"Error: '{project_path}' is not a directory.") + sys.exit(1) + + # Collect files from the project directory + file_list = collect_files(project_path) + + if not file_list: + print("No files found in the project directory with the specified extensions.") + sys.exit(1) + + # Sort file_list for better organization + file_list.sort() + + # Interactive file selection using questionary + print("\nSelect the files you want to include:") + selected_files = questionary.checkbox( + "Press space to select files, and Enter when you're done:", + choices=[str(f) for f in file_list] + ).ask() + + if not selected_files: + print("No files selected.") + sys.exit(1) + + # Generate markdown + markdown_lines = [] + markdown_lines.append('') + + for selected_file in selected_files: + file_path = project_path / selected_file + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + # Determine the language for code block from file extension + language = file_path.suffix.lstrip('.') + markdown_lines.append(f'{selected_file}') + markdown_lines.append(f'```{language}') + markdown_lines.append(content) + markdown_lines.append('```') + markdown_lines.append('') + except Exception as e: + print(f"Error reading file {selected_file}: {e}") + + markdown_text = '\n'.join(markdown_lines) + + # Copy markdown content to clipboard + pyperclip.copy(markdown_text) + print("Markdown content has been copied to the clipboard.") + +if __name__ == "__main__": + # Check if required libraries are installed + try: + import questionary + import pyperclip + except ImportError as e: + missing_module = e.name + print(f"Error: Missing required module '{missing_module}'.") + print(f"Please install it by running: pip install {missing_module}") + sys.exit(1) + + main() diff --git a/scripts/next/config/next.config.build.js b/scripts/next/config/next.config.build.js new file mode 100644 index 0000000..6873c37 --- /dev/null +++ b/scripts/next/config/next.config.build.js @@ -0,0 +1,76 @@ +/* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. + * This is especially useful for Docker builds. + */ +import './src/env.js'; +import { withSentryConfig } from '@sentry/nextjs'; +import { withPlausibleProxy } from 'next-plausible'; + +/** @type {import("next").NextConfig} */ +const config = withPlausibleProxy({ + customDomain: 'https://plausible.gbrown.org', +})({ + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + turbopack: { + rules: { + '*.svg': { + loaders: [ + { + loader: '@svgr/webpack', + options: { + icon: true, + }, + }, + ], + as: '*.js', + }, + }, + }, +}); + +const sentryConfig = { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: 'gib', + project: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, + sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, +}; + +export default withSentryConfig(config, sentryConfig); diff --git a/scripts/next/config/next.config.default.js b/scripts/next/config/next.config.default.js new file mode 100644 index 0000000..bbf2912 --- /dev/null +++ b/scripts/next/config/next.config.default.js @@ -0,0 +1,70 @@ +/* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. + * This is especially useful for Docker builds. + */ +import './src/env.js'; +import { withSentryConfig } from '@sentry/nextjs'; +import { withPlausibleProxy } from 'next-plausible'; + +/** @type {import("next").NextConfig} */ +const config = withPlausibleProxy({ + customDomain: 'https://plausible.gbrown.org', +})({ + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + turbopack: { + rules: { + '*.svg': { + loaders: [ + { + loader: '@svgr/webpack', + options: { + icon: true, + }, + }, + ], + as: '*.js', + }, + }, + }, +}); + +const sentryConfig = { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: 'gib', + project: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, + sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL, + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, +}; + +export default withSentryConfig(config, sentryConfig); diff --git a/scripts/next/docker/Dockerfile b/scripts/next/docker/Dockerfile new file mode 100644 index 0000000..fb02486 --- /dev/null +++ b/scripts/next/docker/Dockerfile @@ -0,0 +1,60 @@ +# syntax=docker/dockerfile:1 +FROM oven/bun:latest AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies with Bun +COPY package.json bun.lockb* ./ +RUN bun install --frozen-lockfile +RUN \ + if [ -f bun.lockb ]; then bun install --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED=1 + +RUN bun run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/scripts/next/docker/compose.yaml b/scripts/next/docker/compose.yaml new file mode 100644 index 0000000..d5306df --- /dev/null +++ b/scripts/next/docker/compose.yaml @@ -0,0 +1,16 @@ +services: + next-template: + build: + context: ../../.. + dockerfile: scripts/next/docker/Dockerfile + image: nextjs + container_name: next-template + networks: + - next-template + ports: + - '3000:3000' + tty: true + restart: unless-stopped +networks: + next-template: + external: true diff --git a/scripts/next/update_container b/scripts/next/update_container new file mode 100755 index 0000000..8a2cf35 --- /dev/null +++ b/scripts/next/update_container @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +git pull +mv ./next.config.js ./scripts/next/config/next.config.default.js +cp ./scripts/next/config/next.config.build.js ./next.config.js +sudo docker compose -f ./scripts/next/docker/compose.yaml down +sudo docker compose -f ./scripts/next/docker/compose.yaml build +sudo docker compose -f ./scripts/next/docker/compose.yaml up -d +cp ./scripts/next/config/next.config.default.js ./next.config.js diff --git a/scripts/supabase/db/schema.sql b/scripts/supabase/db/schema.sql new file mode 100644 index 0000000..d67a532 --- /dev/null +++ b/scripts/supabase/db/schema.sql @@ -0,0 +1,126 @@ +-- Create a table for public profiles +create table profiles ( + id uuid references auth.users on delete cascade not null primary key, + updated_at timestamp with time zone, + email text unique, + full_name text, + avatar_url text, + provider text, + + constraint full_name_length check (char_length(full_name) >= 3 and char_length(full_name) <= 50) +); +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table profiles + enable row level security; + +create policy "Public profiles are viewable by everyone." on profiles + for select using (true); + +create policy "Users can insert their own profile." on profiles + for insert with check ((select auth.uid()) = id); + +create policy "Users can update own profile." on profiles + for update using ((select auth.uid()) = id); + +-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth. +-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. +create function public.handle_new_user() +returns trigger +set search_path = '' +as $$ +begin + insert into public.profiles (id, email, full_name, avatar_url, provider, updated_at) + values ( + new.id, + new.email, + new.raw_user_meta_data->>'full_name', + new.raw_user_meta_data->>'avatar_url', + new.raw_user_meta_data->>'provider', + now() + ); + return new; +end; +$$ language plpgsql security definer; +create trigger on_auth_user_created + after insert on auth.users + for each row execute procedure public.handle_new_user(); + +-- Set up Storage! +insert into storage.buckets (id, name) + values ('avatars', 'avatars'); + +-- Set up access controls for storage. +-- See https://supabase.com/docs/guides/storage#policy-examples for more details. +create policy "Avatar images are publicly accessible." on storage.objects + for select using (bucket_id = 'avatars'); + +create policy "Anyone can upload an avatar." on storage.objects + for insert with check (bucket_id = 'avatars'); + +create policy "Anyone can update an avatar." on storage.objects + for update using (bucket_id = 'avatars'); + +create policy "Anyone can delete an avatar." on storage.objects + for delete using (bucket_id = 'avatars'); + +-- Create a table for public statuses +CREATE TABLE statuses ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + user_id uuid REFERENCES public.profiles ON DELETE CASCADE NOT NULL, + updated_by_id uuid REFERENCES public.profiles ON DELETE SET NULL DEFAULT auth.uid(), + created_at timestamp with time zone DEFAULT now() NOT NULL, + status text NOT NULL, + CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80) +); + +-- Set up Row Level Security (RLS) +ALTER TABLE statuses + ENABLE ROW LEVEL SECURITY; + +-- Policies +CREATE POLICY "Public statuses are viewable by everyone." ON statuses + FOR SELECT USING (true); + +-- RECREATE it using the recommended sub-select form +CREATE POLICY "Authenticated users can insert statuses for any user." + ON public.statuses + FOR INSERT + WITH CHECK ( + (SELECT auth.role()) = 'authenticated' + ); + +-- ADD an UPDATE policy so anyone signed-in can update *any* status +CREATE POLICY "Authenticated users can update statuses for any user." + ON public.statuses + FOR UPDATE + USING ( + (SELECT auth.role()) = 'authenticated' + ) + WITH CHECK ( + (SELECT auth.role()) = 'authenticated' + ); + +-- Function to add first status +CREATE FUNCTION public.handle_first_status() +RETURNS TRIGGER +SET search_path = '' +AS $$ +BEGIN + INSERT INTO public.statuses (user_id, updated_by_id, status) + VALUES ( + NEW.id, + NEW.id, + 'Just joined!' + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Create a separate trigger for the status +CREATE TRIGGER on_auth_user_created_add_status + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status(); + +alter publication supabase_realtime add table profiles; +alter publication supabase_realtime add table statuses; diff --git a/scripts/supabase/docker/.env.example b/scripts/supabase/docker/.env.example new file mode 100644 index 0000000..96e8034 --- /dev/null +++ b/scripts/supabase/docker/.env.example @@ -0,0 +1,158 @@ +############ +# Secrets +# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION +############ + +POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password +JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long +ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE +SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q +DASHBOARD_USERNAME=gib +DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated +SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq +VAULT_ENC_KEY=your-encryption-key-32-chars-min + + +############ +# Database - You can change these to any PostgreSQL database that has logical replication enabled. +############ + +POSTGRES_HOST=db +POSTGRES_DB=postgres +POSTGRES_PORT=5432 +# default user is postgres + + +############ +# Supavisor -- Database pooler +############ +POOLER_PROXY_PORT_TRANSACTION=6543 +POOLER_DEFAULT_POOL_SIZE=20 +POOLER_MAX_CLIENT_CONN=100 +POOLER_TENANT_ID=your-tenant-id # Change me + + +############ +# API Proxy - Configuration for the Kong Reverse proxy. +############ + +KONG_HTTP_PORT=8000 +KONG_HTTPS_PORT=8443 + + +############ +# API - Configuration for PostgREST. +############ + +PGRST_DB_SCHEMAS=public,storage,graphql_public + + +############ +# Auth - Configuration for the GoTrue authentication server. +############ + +## General +SITE_URL=http://localhost:3000 # Change to URL of site used for email links/auth flows +ADDITIONAL_REDIRECT_URLS= # Change to include any redirect URIs needed +JWT_EXPIRY=3600 +DISABLE_SIGNUP=false +API_EXTERNAL_URL=http://localhost:8000 # Should be the same as the SITE URL usually. + +## Mailer Config +MAILER_URLPATHS_CONFIRMATION="/auth/callback" +MAILER_URLPATHS_INVITE="/auth/callback" +MAILER_URLPATHS_RECOVERY="/auth/callback" +MAILER_URLPATHS_EMAIL_CHANGE="/auth/callback" + +## Email auth +ENABLE_EMAIL_SIGNUP=true +ENABLE_EMAIL_AUTOCONFIRM=false +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_HOST=supabase-mail +SMTP_PORT=2500 +SMTP_USER=fake_mail_user +SMTP_PASS=fake_mail_password +SMTP_SENDER_NAME=fake_sender +ENABLE_ANONYMOUS_USERS=false + + +MAILER_TEMPLATES_INVITE="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/invite_user.html" +MAILER_TEMPLATES_CONFIRMATION="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/confirm_signup.html" +MAILER_TEMPLATES_RECOVERY="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/reset_password.html" +MAILER_TEMPLATES_MAGIC_LINK="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/magic_link.html" +MAILER_TEMPLATES_EMAIL_CHANGE="https://git.gbrown.org/gib/tech-tracker-next/raw/branch/main/src/server/mail_templates/change_email_address.html" + +MAILER_SUBJECTS_INVITE="You've Been Invited!" +MAILER_SUBJECTS_CONFIRMATION="Confirm Your Email" +MAILER_SUBJECTS_RECOVERY="Reset Password" +MAILER_SUBJECTS_MAGIC_LINK="Magic Sign In Link" +MAILER_SUBJECTS_EMAIL_CHANGE="Change Email Address" + + +## Phone auth +ENABLE_PHONE_SIGNUP=false +ENABLE_PHONE_AUTOCONFIRM=false + + +# Apple Auth +APPLE_ENABLED=true +APPLE_CLIENT_ID= +APPLE_SECRET= +APPLE_REDIRECT_URI= +APPLE_TEAM_ID= +APPLE_KEY_ID= + +# Azure Auth +AZURE_ENABLED=true +AZURE_CLIENT_ID= +AZURE_SECRET= +AZURE_REDIRECT_URI= +AZURE_TENANT_ID= +AZURE_TENANT_URL= + +# Gib's Auth (Trying to set up Authentik) +#SAML_ENABLED=false +#SAML_PRIVATE_KEY= + + +############ +# Studio - Configuration for the Dashboard +############ + +STUDIO_DEFAULT_ORGANIZATION=gbrown +STUDIO_DEFAULT_PROJECT=Default Project + +STUDIO_PORT=3000 +# replace if you intend to use Studio outside of localhost +SUPABASE_PUBLIC_URL=https://localhost:8000 # Change to URL for this supabase instance + +# Enable webp support +IMGPROXY_ENABLE_WEBP_DETECTION=true + +# Add your OpenAI API key to enable SQL Editor Assistant +OPENAI_API_KEY= + + +############ +# Functions - Configuration for Functions +############ +# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet. +FUNCTIONS_VERIFY_JWT=false + + +############ +# Logs - Configuration for Logflare +# Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction +############ + +LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key + +# Change vector.toml sinks to reflect this change +LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key + +# Docker socket location - this value will differ depending on your OS +DOCKER_SOCKET_LOCATION=/var/run/docker.sock + +# Google Cloud Project details +#GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID +#GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER diff --git a/scripts/supabase/docker/docker-compose.dev.yml b/scripts/supabase/docker/docker-compose.dev.yml new file mode 100644 index 0000000..85cb604 --- /dev/null +++ b/scripts/supabase/docker/docker-compose.dev.yml @@ -0,0 +1,41 @@ +networks: + supabase-network: + name: supabase-network + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 +services: + studio: + build: + context: . + dockerfile: studio/Dockerfile + target: dev + networks: [supabase-network] + ports: + - 8082:8082 + mail: + container_name: supabase-mail + image: inbucket/inbucket:3.0.3 + networks: [supabase-network] + ports: + - '2500:2500' # SMTP + - '9000:9000' # web interface + - '1100:1100' # POP3 + auth: + environment: + - GOTRUE_SMTP_USER= + - GOTRUE_SMTP_PASS= + meta: + ports: + - 5555:8080 + db: + restart: 'no' + volumes: + # Always use a fresh database when developing + - /var/lib/postgresql/data + # Seed data should be inserted last (alphabetical order) + - ../db/schema.sql:/docker-entrypoint-initdb.d/seed.sql + storage: + volumes: + - /var/lib/storage diff --git a/scripts/supabase/docker/docker-compose.s3.yml b/scripts/supabase/docker/docker-compose.s3.yml new file mode 100644 index 0000000..18c7866 --- /dev/null +++ b/scripts/supabase/docker/docker-compose.s3.yml @@ -0,0 +1,105 @@ + +networks: + supabase-network: + name: supabase-network + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 +services: + minio: + image: minio/minio + networks: [supabase-network] + ports: + - '9000:9000' + - '9001:9001' + environment: + MINIO_ROOT_USER: supa-storage + MINIO_ROOT_PASSWORD: secret1234 + command: server --console-address ":9001" /data + healthcheck: + test: [ "CMD", "curl", "-f", "http://minio:9000/minio/health/live" ] + interval: 2s + timeout: 10s + retries: 5 + volumes: + - ./volumes/storage:/data:z + + minio-createbucket: + image: minio/mc + networks: [supabase-network] + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set supa-minio http://minio:9000 supa-storage secret1234; + /usr/bin/mc mb supa-minio/stub; + exit 0; + " + + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.11.13 + networks: [supabase-network] + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + minio: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:5000/status" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: s3 + GLOBAL_S3_BUCKET: stub + GLOBAL_S3_ENDPOINT: http://minio:9000 + GLOBAL_S3_PROTOCOL: http + GLOBAL_S3_FORCE_PATH_STYLE: true + AWS_ACCESS_KEY_ID: supa-storage + AWS_SECRET_ACCESS_KEY: secret1234 + AWS_DEFAULT_REGION: stub + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - ./volumes/storage:/var/lib/storage:z + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + networks: [supabase-network] + healthcheck: + test: [ "CMD", "imgproxy", "health" ] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} diff --git a/scripts/supabase/docker/docker-compose.yml b/scripts/supabase/docker/docker-compose.yml new file mode 100644 index 0000000..2136295 --- /dev/null +++ b/scripts/supabase/docker/docker-compose.yml @@ -0,0 +1,579 @@ +# Usage +# Start: docker compose up +# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +# Stop: docker compose down +# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans +# Reset everything: ./reset.sh + +name: techtracker + +networks: + techtracker: + name: techtracker + driver: bridge + ipam: + config: + - subnet: 172.19.0.0/16 + +services: + + studio: + container_name: supabase-studio + image: supabase/studio:2025.05.19-sha-3487831 + networks: [techtracker] + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})" + ] + timeout: 10s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + # Comment to use Big Query backend for analytics + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + # Uncomment to use Big Query backend for analytics + # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + + kong: + container_name: supabase-kong + image: kong:2.8.1 + networks: [techtracker] + restart: unless-stopped + ports: + - ${KONG_HTTP_PORT}:8000/tcp + - ${KONG_HTTPS_PORT}:8443/tcp + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + # https://unix.stackexchange.com/a/294837 + 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.172.1 + networks: [techtracker] + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + 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}@${POSTGRES_HOST}:${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} + + # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. + # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true + + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true + # GOTRUE_SMTP_MAX_FREQUENCY: 1s + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + + GOTRUE_MAILER_TEMPLATES_INVITE: ${MAILER_TEMPLATES_INVITE} + GOTRUE_MAILER_TEMPLATES_CONFIRMATION: ${MAILER_TEMPLATES_CONFIRMATION} + GOTRUE_MAILER_TEMPLATES_RECOVERY: ${MAILER_TEMPLATES_RECOVERY} + GOTRUE_MAILER_TEMPLATES_MAGIC_LINK: ${MAILER_TEMPLATES_MAGIC_LINK} + GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE: ${MAILER_TEMPLATES_EMAIL_CHANGE} + + GOTRUE_MAILER_SUBJECTS_CONFIRMATION: ${MAILER_SUBJECTS_CONFIRMATION} + GOTRUE_MAILER_SUBJECTS_RECOVERY: ${MAILER_SUBJECTS_RECOVERY} + GOTRUE_MAILER_SUBJECTS_MAGIC_LINK: ${MAILER_SUBJECTS_MAGIC_LINK} + GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGE: ${MAILER_SUBJECTS_EMAIL_CHANGE} + GOTRUE_MAILER_SUBJECTS_INVITE: ${MAILER_SUBJECTS_INVITE} + + GOTRUE_EXTERNAL_APPLE_ENABLED: ${APPLE_ENABLED} + GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${APPLE_CLIENT_ID} + GOTRUE_EXTERNAL_APPLE_SECRET: ${APPLE_SECRET} + GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${APPLE_REDIRECT_URI} + + GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED} + GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID} + GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET} + GOTRUE_EXTERNAL_AZURE_TENANT_ID: ${AZURE_TENANT_ID} + GOTRUE_EXTERNAL_AZURE_URL: ${AZURE_TENANT_URL} + GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${AZURE_REDIRECT_URI} + + # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook + + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: "" + + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/mfa_verification_attempt" + + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: "true" + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/password_verification_attempt" + + # GOTRUE_HOOK_SEND_SMS_ENABLED: "false" + # GOTRUE_HOOK_SEND_SMS_URI: "pg-functions://postgres/public/custom_access_token_hook" + # GOTRUE_HOOK_SEND_SMS_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + # GOTRUE_HOOK_SEND_EMAIL_ENABLED: "false" + # GOTRUE_HOOK_SEND_EMAIL_URI: "http://host.docker.internal:54321/functions/v1/email_sender" + # GOTRUE_HOOK_SEND_EMAIL_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v12.2.12 + networks: [techtracker] + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${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" + ] + + realtime: + # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.34.47 + networks: [techtracker] + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer ${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health" + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: true + RUN_JANITOR: true + + # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.22.17 + networks: [techtracker] + restart: unless-stopped + volumes: + - ./volumes/storage:/var/lib/storage:z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://storage:5000/status" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + networks: [techtracker] + restart: unless-stopped + volumes: + - ./volumes/storage:/var/lib/storage:z + healthcheck: + test: + [ + "CMD", + "imgproxy", + "health" + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.89.0 + networks: [techtracker] + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.67.4 + networks: [techtracker] + restart: unless-stopped + volumes: + - ./volumes/functions:/home/deno/functions:Z + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + command: + [ + "start", + "--main-service", + "/home/deno/functions/main" + ] + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.12.0 + networks: [techtracker] + restart: unless-stopped + ports: + - 4000:4000 + # Uncomment to use Big Query backend for analytics + # volumes: + # - type: bind + # source: ${PWD}/gcloud.json + # target: /opt/app/rel/logflare/bin/gcloud.json + # read_only: true + healthcheck: + test: + [ + "CMD", + "curl", + "http://localhost:4000/health" + ] + timeout: 5s + interval: 5s + retries: 10 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: _supabase + DB_HOSTNAME: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + + # Comment variables to use Big Query backend for analytics + POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + # Uncomment to use Big Query backend for analytics + # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} + # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} + + # Comment out everything below this point if you are using an external Postgres database + db: + container_name: supabase-db + image: supabase/postgres:15.8.1.060 + networks: [techtracker] + ports: + - ${POSTGRES_PORT}:${POSTGRES_PORT} + restart: unless-stopped + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + # Must be superuser to create event trigger + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + # Must be superuser to alter reserved role + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + # PGDATA directory is persisted between restarts + - ./volumes/db/data:/var/lib/postgresql/data:Z + # Changes required for internal supabase data such as _analytics + - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z + # Changes required for Analytics support + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + # Changes required for Pooler support + - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z + # Initial SQL that should run + - ../db/schema.sql:/docker-entrypoint-initdb.d/seed.sql + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + healthcheck: + test: + [ + "CMD", + "pg_isready", + "-U", + "postgres", + "-h", + "localhost" + ] + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + 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" # prevents Realtime polling queries from appearing in logs + ] + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + networks: [techtracker] + restart: unless-stopped + volumes: + - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z + - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health" + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + command: + [ + "--config", + "/etc/vector/vector.yml" + ] + security_opt: + - "label=disable" + + # Update the DATABASE_URL if you are using an external Postgres database + supavisor: + container_name: supabase-pooler + image: supabase/supavisor:2.5.1 + networks: [techtracker] + restart: unless-stopped + ports: + #- ${POSTGRES_PORT}:5432 + - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + volumes: + - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "http://127.0.0.1:4000/api/health" + ] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + PORT: 4000 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase + CLUSTER_POSTGRES: true + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + VAULT_ENC_KEY: ${VAULT_ENC_KEY} + API_JWT_SECRET: ${JWT_SECRET} + METRICS_JWT_SECRET: ${JWT_SECRET} + REGION: local + ERL_AFLAGS: -proto_dist inet_tcp + POOLER_TENANT_ID: ${POOLER_TENANT_ID} + POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} + POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} + POOLER_POOL_MODE: transaction + command: + [ + "/bin/sh", + "-c", + "/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server" + ] + +volumes: + db-config: + name: techtracker-config diff --git a/scripts/supabase/docker/volumes/api/kong.yml b/scripts/supabase/docker/volumes/api/kong.yml new file mode 100644 index 0000000..7abf425 --- /dev/null +++ b/scripts/supabase/docker/volumes/api/kong.yml @@ -0,0 +1,241 @@ +_format_version: '2.1' +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: $DASHBOARD_USERNAME + password: $DASHBOARD_PASSWORD + +### +### API Routes +### +services: + ## Open Auth routes + - 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 + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + 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 + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + 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 + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' + url: http://functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + url: http://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/scripts/supabase/docker/volumes/db/_supabase.sql b/scripts/supabase/docker/volumes/db/_supabase.sql new file mode 100644 index 0000000..6236ae1 --- /dev/null +++ b/scripts/supabase/docker/volumes/db/_supabase.sql @@ -0,0 +1,3 @@ +\set pguser `echo "$POSTGRES_USER"` + +CREATE DATABASE _supabase WITH OWNER :pguser; diff --git a/scripts/supabase/docker/volumes/db/jwt.sql b/scripts/supabase/docker/volumes/db/jwt.sql new file mode 100644 index 0000000..cfd3b16 --- /dev/null +++ b/scripts/supabase/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/scripts/supabase/docker/volumes/db/logs.sql b/scripts/supabase/docker/volumes/db/logs.sql new file mode 100644 index 0000000..255c0f4 --- /dev/null +++ b/scripts/supabase/docker/volumes/db/logs.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; +\c postgres diff --git a/scripts/supabase/docker/volumes/db/pooler.sql b/scripts/supabase/docker/volumes/db/pooler.sql new file mode 100644 index 0000000..162c5b9 --- /dev/null +++ b/scripts/supabase/docker/volumes/db/pooler.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _supavisor; +alter schema _supavisor owner to :pguser; +\c postgres diff --git a/scripts/supabase/docker/volumes/db/realtime.sql b/scripts/supabase/docker/volumes/db/realtime.sql new file mode 100644 index 0000000..4d4b9ff --- /dev/null +++ b/scripts/supabase/docker/volumes/db/realtime.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/scripts/supabase/docker/volumes/db/roles.sql b/scripts/supabase/docker/volumes/db/roles.sql new file mode 100644 index 0000000..8f7161a --- /dev/null +++ b/scripts/supabase/docker/volumes/db/roles.sql @@ -0,0 +1,8 @@ +-- 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_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; diff --git a/scripts/supabase/docker/volumes/db/webhooks.sql b/scripts/supabase/docker/volumes/db/webhooks.sql new file mode 100644 index 0000000..5837b86 --- /dev/null +++ b/scripts/supabase/docker/volumes/db/webhooks.sql @@ -0,0 +1,208 @@ +BEGIN; + -- Create pg_net extension + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + -- Create supabase_functions schema + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + -- supabase_functions.migrations definition + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + -- Initial supabase_functions migration + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + -- supabase_functions.hooks definition + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get( + url, + params, + headers, + timeout_ms + ); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, + 'record', NEW, + 'type', TG_OP, + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA + ); + + SELECT http_post INTO request_id FROM net.http_post( + url, + payload, + params, + headers, + timeout_ms + ); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + + INSERT INTO supabase_functions.hooks + (hook_table_id, hook_name, request_id) + VALUES + (TG_RELID, TG_NAME, request_id); + + RETURN NEW; + END + $function$; + -- Supabase super admin + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + -- Remove unused supabase_pg_net_admin role + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_pg_net_admin' + ) + THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + -- pg_net grants when extension is already enabled + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + -- Event trigger for pg_net + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_event_trigger + WHERE evtname = 'issue_pg_net_access' + ) THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; diff --git a/scripts/supabase/docker/volumes/functions/hello/index.ts b/scripts/supabase/docker/volumes/functions/hello/index.ts new file mode 100644 index 0000000..7ae5cc1 --- /dev/null +++ b/scripts/supabase/docker/volumes/functions/hello/index.ts @@ -0,0 +1,15 @@ +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +import { serve } from 'https://deno.land/std@0.177.1/http/server.ts'; + +serve(async () => { + return new Response(`"Hello from Edge Functions!"`, { + headers: { 'Content-Type': 'application/json' }, + }); +}); + +// To invoke: +// curl 'http://localhost:/functions/v1/hello' \ +// --header 'Authorization: Bearer ' diff --git a/scripts/supabase/docker/volumes/functions/main/index.ts b/scripts/supabase/docker/volumes/functions/main/index.ts new file mode 100644 index 0000000..291c1e0 --- /dev/null +++ b/scripts/supabase/docker/volumes/functions/main/index.ts @@ -0,0 +1,94 @@ +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'; +import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'; + +console.log('main function started'); + +const JWT_SECRET = Deno.env.get('JWT_SECRET'); +const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'; + +function getAuthToken(req: Request) { + const authHeader = req.headers.get('authorization'); + if (!authHeader) { + throw new Error('Missing authorization header'); + } + const [bearer, token] = authHeader.split(' '); + if (bearer !== 'Bearer') { + throw new Error(`Auth header is not 'Bearer {token}'`); + } + return token; +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET); + try { + await jose.jwtVerify(jwt, secretKey); + } catch (err) { + console.error(err); + return false; + } + return true; +} + +serve(async (req: Request) => { + if (req.method !== 'OPTIONS' && VERIFY_JWT) { + try { + const token = getAuthToken(req); + const isValidJWT = await verifyJWT(token); + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + } catch (e) { + console.error(e); + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + } + + const url = new URL(req.url); + const { pathname } = url; + const path_parts = pathname.split('/'); + const service_name = path_parts[1]; + + if (!service_name || service_name === '') { + const error = { msg: 'missing function name in request' }; + return new Response(JSON.stringify(error), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const servicePath = `/home/deno/functions/${service_name}`; + console.error(`serving the request with ${servicePath}`); + + const memoryLimitMb = 150; + const workerTimeoutMs = 1 * 60 * 1000; + const noModuleCache = false; + const importMapPath = null; + const envVarsObj = Deno.env.toObject(); + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]); + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }); + return await worker.fetch(req); + } catch (e) { + const error = { msg: e.toString() }; + return new Response(JSON.stringify(error), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +}); diff --git a/scripts/supabase/docker/volumes/logs/vector.yml b/scripts/supabase/docker/volumes/logs/vector.yml new file mode 100644 index 0000000..cce46df --- /dev/null +++ b/scripts/supabase/docker/volumes/logs/vector.yml @@ -0,0 +1,232 @@ +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + exclude_containers: + - supabase-vector + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-functions"' + db: '.appname == "supabase-db"' + # Ignores non nginx errors since they are related with kong booting up + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.request.method = req.method + .metadata.request.path = req.path + .metadata.request.protocol = req.protocol + .metadata.response.status_code = req.status + } + if err != null { + abort + } + # Ignores non nginx errors since they are related with kong booting up + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + # PostgREST logs are structured so we separate timestamp from message using regex + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P

+ + diff --git a/scripts/supabase/mail_templates/confirm_signup.html b/scripts/supabase/mail_templates/confirm_signup.html new file mode 100644 index 0000000..f068df8 --- /dev/null +++ b/scripts/supabase/mail_templates/confirm_signup.html @@ -0,0 +1,41 @@ + + + + Confirm Your Email + + +
+
+ + + + + +
+ Tech Tracker Logo + +

Tech Tracker

+
+
+ +

Confirm Your Email

+ +

Hello,

+ +

Thank you for signing up for Tech Tracker. To complete your registration, please confirm your email address by clicking the button below:

+ + + +

If you didn't create an account with Tech Tracker, you can safely ignore this email.

+ +
+

Tech Tracker - City of Gulfport

+
+
+ + diff --git a/scripts/supabase/mail_templates/invite_user.html b/scripts/supabase/mail_templates/invite_user.html new file mode 100644 index 0000000..7d4c1ea --- /dev/null +++ b/scripts/supabase/mail_templates/invite_user.html @@ -0,0 +1,41 @@ + + + + You've Been Invited! + + +
+
+ + + + + +
+ Tech Tracker Logo + +

Tech Tracker

+
+
+ +

You've Been Invited

+ +

Hello,

+ +

You have been invited to join Tech Tracker. To accept this invitation and create your account, please click the button below:

+ + + +

Tech Tracker helps teams manage their projects efficiently. We're excited to have you on board!

+ +
+

Tech Tracker - City of Gulfport

+
+
+ + diff --git a/scripts/supabase/mail_templates/magic_link.html b/scripts/supabase/mail_templates/magic_link.html new file mode 100644 index 0000000..9dde3dc --- /dev/null +++ b/scripts/supabase/mail_templates/magic_link.html @@ -0,0 +1,43 @@ + + + + Magic Sign In Link + + +
+
+ + + + + +
+ Tech Tracker Logo + +

Tech Tracker

+
+
+ +

Your Magic Link

+ +

Hello,

+ +

You requested a magic link to sign in to your Tech Tracker account. Click the button below to sign in:

+ + + +

This link will expire in 1 hour and can only be used once.

+ +

If you didn't request this magic link, you can safely ignore this email.

+ +
+

Tech Tracker - City of Gulfport

+
+
+ + diff --git a/scripts/supabase/mail_templates/reauthentication.html b/scripts/supabase/mail_templates/reauthentication.html new file mode 100644 index 0000000..6b5d97c --- /dev/null +++ b/scripts/supabase/mail_templates/reauthentication.html @@ -0,0 +1,42 @@ + + + + Confirm Reauthentication + + +
+
+ + + + + +
+ Tech Tracker Logo + +

Tech Tracker

+
+
+ +

Confirm Reauthentication

+ +

Hello,

+ +

For security reasons, we need to verify your identity. Please enter the following code when prompted:

+ +
+
+ {{ .Token }} +
+
+ +

This code will expire in 10 minutes.

+ +

If you didn't request this code, please secure your account by changing your password immediately.

+ +
+

Tech Tracker - City of Gulfport

+
+
+ + diff --git a/scripts/supabase/mail_templates/reset_password.html b/scripts/supabase/mail_templates/reset_password.html new file mode 100644 index 0000000..4b6a41b --- /dev/null +++ b/scripts/supabase/mail_templates/reset_password.html @@ -0,0 +1,43 @@ + + + + Reset Password + + +
+
+ + + + + +
+ Tech Tracker Logo + +

Tech Tracker

+
+
+ +

Reset Your Password

+ +

Hello,

+ +

We received a request to reset your password for your Tech Tracker account. Follow this link to reset the password for your user:

+ + + +

If you didn't request a password reset, you can safely ignore this email.

+ +

This link will expire in 1 hour.

+ +
+

Tech Tracker - City of Gulfport

+
+
+ + diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 0000000..c30c1d7 --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,9 @@ +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ +import * as Sentry from '@sentry/nextjs'; +import './src/env.js'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1, // Define how likely traces are sampled or use tracesSampler for more control. + debug: false, // Print useful debugging info while setting up Sentry. +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..2d79c7f --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,25 @@ +import '@/styles/globals.css'; + +import { type Metadata } from 'next'; +import { Geist } from 'next/font/google'; + +export const metadata: Metadata = { + title: 'Create T3 App', + description: 'Generated by create-t3-app', + icons: [{ rel: 'icon', url: '/favicon.ico' }], +}; + +const geist = Geist({ + subsets: ['latin'], + variable: '--font-geist-sans', +}); + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..f2b8d37 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link'; + +export default function HomePage() { + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+ ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..52e6be0 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..8375444 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/src/components/ui/based-avatar.tsx b/src/components/ui/based-avatar.tsx new file mode 100644 index 0000000..440213e --- /dev/null +++ b/src/components/ui/based-avatar.tsx @@ -0,0 +1,57 @@ +'use client'; +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { User } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { AvatarImage } from '@/components/ui/avatar'; + +type BasedAvatarProps = React.ComponentProps & { + src?: string | null; + fullName?: string | null; + imageClassName?: string; + fallbackClassName?: string; + userIconSize?: number; +}; + +export const BasedAvatar = ({ + src = null, + fullName = null, + imageClassName = '', + fallbackClassName = '', + userIconSize = 32, + className, + ...props +}: BasedAvatarProps) => { + return ( + + {src ? ( + + ) : ( + + {fullName ? ( + fullName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + ) : ( + + )} + + )} + + ); +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..b012b53 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + xl: 'h-12 rounded-md px-8 has-[>svg]:px-6', + xxl: 'h-14 rounded-md px-10 has-[>svg]:px-8', + icon: 'size-9', + smicon: 'size-6', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..aba18f7 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,210 @@ +'use client'; + +import * as React from 'react'; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from 'lucide-react'; +import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'; + +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = 'label', + buttonVariant = 'ghost', + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps['variant']; +}) { + const defaultClassNames = getDefaultClassNames(); + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString('default', { month: 'short' }), + ...formatters, + }} + classNames={{ + root: cn('w-fit', defaultClassNames.root), + months: cn( + 'flex gap-4 flex-col md:flex-row relative', + defaultClassNames.months, + ), + month: cn('flex flex-col w-full gap-4', defaultClassNames.month), + nav: cn( + 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between', + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', + defaultClassNames.button_next, + ), + month_caption: cn( + 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)', + defaultClassNames.month_caption, + ), + dropdowns: cn( + 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5', + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md', + defaultClassNames.dropdown_root, + ), + dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown), + caption_label: cn( + 'select-none font-medium', + captionLayout === 'label' + ? 'text-sm' + : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5', + defaultClassNames.caption_label, + ), + table: 'w-full border-collapse', + weekdays: cn('flex', defaultClassNames.weekdays), + weekday: cn( + 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none', + defaultClassNames.weekday, + ), + week: cn('flex w-full mt-2', defaultClassNames.week), + week_number_header: cn( + 'select-none w-(--cell-size)', + defaultClassNames.week_number_header, + ), + week_number: cn( + 'text-[0.8rem] select-none text-muted-foreground', + defaultClassNames.week_number, + ), + day: cn( + 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none', + defaultClassNames.day, + ), + range_start: cn( + 'rounded-l-md bg-accent', + defaultClassNames.range_start, + ), + range_middle: cn('rounded-none', defaultClassNames.range_middle), + range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end), + today: cn( + 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none', + defaultClassNames.today, + ), + outside: cn( + 'text-muted-foreground aria-selected:text-muted-foreground', + defaultClassNames.outside, + ), + disabled: cn( + 'text-muted-foreground opacity-50', + defaultClassNames.disabled, + ), + hidden: cn('invisible', defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === 'left') { + return ( + + ); + } + + if (orientation === 'right') { + return ( + + ); + } + + return ( + + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); + }, + ...components, + }} + {...props} + /> + ); +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( + + + + + + + {`No ${optionName} found.`} + + {selectOptions.map((selectOption) => ( + { + setValue(currentValue === value ? '' : currentValue); + setOpen(false); + }} + > + + {selectOption.label} + + ))} + + + + + + ); +}; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..62a2627 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,184 @@ +'use client'; + +import * as React from 'react'; +import { Command as CommandPrimitive } from 'cmdk'; +import { SearchIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandDialog({ + title = 'Command Palette', + description = 'Search for a command to run...', + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string; + description?: string; + className?: string; + showCloseButton?: boolean; +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ); +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ); +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..e481c76 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..0e2eb3c --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,135 @@ +'use client'; + +import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; + +import { cn } from '@/lib/utils'; + +function Drawer({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ); +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..7a8804e --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..5dd2131 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,168 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId(); + + return ( + +
+ + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( +