diff --git a/Tiltfile b/Tiltfile index 8cd535a..510df62 100644 --- a/Tiltfile +++ b/Tiltfile @@ -188,13 +188,13 @@ k8s_yaml(helm( # ## before you think of switching to valkey, dragonfly, or one of the other redis alternatives, STOP. Uppy is picky. # ## I tested dragonfly, valkey, and KeyDB. Uppy's ioredis client was unable to connect. "ECONNREFUSED" ... # ## Uppy was only happy connecting to official redis. -k8s_yaml(helm( - './charts/redis/redis', - namespace='futureporn', - values=[ - './charts/redis/values-overrides.yaml' - ] -)) +# k8s_yaml(helm( +# './charts/redis/redis', +# namespace='futureporn', +# values=[ +# './charts/redis/values-overrides.yaml' +# ] +# )) k8s_yaml(helm( './charts/cert-manager/cert-manager', @@ -290,33 +290,33 @@ cmd_button('postgres:drop', icon_name='delete', text='DROP all databases' ) -cmd_button('postgres:refresh', +cmd_button('migrations-schema:refresh', argv=['echo', '@todo please restart postgrest container manually.'], - resource='migrations', + resource='migrations-schema', icon_name='refresh', text='Refresh schema cache' ) - -## @todo let's make this get a random room from scout then use the random room to record via POST /recordings -cmd_button('capture-worker:create', - argv=['./scripts/capture-integration.sh'], - resource='capture-worker', - icon_name='send', - text='Recording Integration Test' +cmd_button('migrations-data:refresh', + argv=['echo', '@todo please restart postgrest container manually.'], + resource='migrations-data', + icon_name='refresh', + text='Refresh schema cache' ) -# cmd_button('drupal:init', -# argv=['./scripts/drupal-init-wrapper.sh'], -# resource='drupal', +## @todo let's make this get a random room from scout then use the random room to record via POST /recordings +# cmd_button('capture-worker:create', +# argv=['./scripts/capture-integration.sh'], +# resource='capture-worker', # icon_name='send', -# text='Initialize Drupal' +# text='Recording Integration Test' +# ) +# k8s_resource( +# workload='capture-worker', +# labels=['backend'], +# resource_deps=['postgrest', 'postgresql-primary'], # ) -cmd_button('postgres:migrate', - argv=['./scripts/postgres-migrations.sh'], - resource='postgresql-primary', - icon_name='directions_run', - text='Run migrations', -) + + cmd_button('pgadmin4:restore', argv=['./scripts/pgadmin-import-connection.sh'], @@ -335,17 +335,31 @@ cmd_button('build:test', ## instead of being invoked by helm, we start a container using this image manually via Tilt UI # update_settings(suppress_unused_image_warnings=["fp/migrations"]) docker_build( - 'fp/migrations', + 'fp/migrations-schema', '.', - dockerfile='dockerfiles/migrations.dockerfile', - target='migrations', + dockerfile='dockerfiles/migrations-schema.dockerfile', + target='migrations-schema', pull=False, only=[ './.npmrc', './package.json', './pnpm-lock.yaml', './pnpm-workspace.yaml', - './services/migrations' + './services/migrations-schema' + ], +) +docker_build( + 'fp/migrations-data', + '.', + dockerfile='dockerfiles/migrations-data.dockerfile', + target='migrations-data', + pull=False, + only=[ + './.npmrc', + './package.json', + './pnpm-lock.yaml', + './pnpm-workspace.yaml', + './services/migrations-data' ], ) @@ -362,16 +376,7 @@ docker_build( pull=False, ) -docker_build( - 'fp/factory', - '.', - dockerfile='./dockerfiles/factory.dockerfile', - target='dev', - live_update=[ - sync('./services/factory', '/app/services/factory') - ], - pull=False, -) + @@ -404,46 +409,46 @@ docker_build( -docker_build( - 'fp/capture', - '.', - dockerfile='dockerfiles/capture.dockerfile', - target='dev', - only=[ - './.npmrc', - './package.json', - './pnpm-lock.yaml', - './pnpm-workspace.yaml', - './packages/types', - './packages/utils', - './packages/fetchers', - './services/capture', - ], - live_update=[ - sync('./services/capture', '/app/services/capture'), - ], - pull=False, -) +# docker_build( +# 'fp/capture', +# '.', +# dockerfile='dockerfiles/capture.dockerfile', +# target='dev', +# only=[ +# './.npmrc', +# './package.json', +# './pnpm-lock.yaml', +# './pnpm-workspace.yaml', +# './packages/types', +# './packages/utils', +# './packages/fetchers', +# './services/capture', +# ], +# live_update=[ +# sync('./services/capture', '/app/services/capture'), +# ], +# pull=False, +# ) -k8s_resource( - workload='scout', - resource_deps=['postgresql-primary'], - # port_forwards=['8134'], - labels=['backend'], -) -k8s_resource( - workload='uppy', - links=[ - link('https://uppy.fp.sbtp.xyz'), - ], - resource_deps=['redis-master'], - labels=['backend'], -) +# k8s_resource( +# workload='scout', +# resource_deps=['postgresql-primary'], +# # port_forwards=['8134'], +# labels=['backend'], +# ) +# k8s_resource( +# workload='uppy', +# links=[ +# link('https://uppy.fp.sbtp.xyz'), +# ], +# resource_deps=['redis-master'], +# labels=['backend'], +# ) k8s_resource( workload='next', links=[ @@ -473,58 +478,19 @@ k8s_resource( labels=['database'] ) -# k8s_resource( -# workload='mariadb', -# labels=['database'] -# ) -# k8s_resource( -# workload='drupal', -# resource_deps=['mariadb'], -# labels=['backend'], -# port_forwards=['9797:8080'], -# links=[ -# link('https://drupal.fp.sbtp.xyz'), -# ], -# ) -k8s_resource( - workload='chart-velero', - resource_deps=['postgresql-primary'], - labels=['backend'], -) -k8s_resource( - workload='chart-velero-upgrade-crds', - resource_deps=['postgresql-primary'], - labels=['backend'], -) - # k8s_resource( -# workload='logto', -# port_forwards=['3001', '3002'], -# links=[ -# link('https://logto.fp.sbtp.xyz'), -# link('https://logto-admin.fp.sbtp.xyz'), -# ], +# workload='chart-velero', +# resource_deps=['postgresql-primary'], # labels=['backend'], # ) # k8s_resource( -# workload='logto-database-seed', -# labels=['database'], -# ) -# k8s_resource( -# workload='phpmyadmin', -# port_forwards=['5151:8080'], -# labels=['database'], -# ) - -# k8s_resource( -# workload='supertokens', -# links=[ -# link('https://supertokens.fp.sbtp.xyz'), -# ], +# workload='chart-velero-upgrade-crds', +# resource_deps=['postgresql-primary'], # labels=['backend'], # ) + k8s_resource( workload='keycloak', links=[ @@ -583,21 +549,27 @@ k8s_resource( workload='factory', labels=['backend'], ) +docker_build( + 'fp/factory', + '.', + dockerfile='./dockerfiles/factory.dockerfile', + target='dev', + live_update=[ + sync('./services/factory', '/app/services/factory') + ], + pull=False, +) + +# k8s_resource( +# workload='redis-master', +# labels=['cache'] +# ) +# k8s_resource( +# workload='bot', +# labels=['backend'], +# resource_deps=['postgrest'], +# ) -k8s_resource( - workload='redis-master', - labels=['cache'] -) -k8s_resource( - workload='bot', - labels=['backend'], - resource_deps=['postgrest'], -) -k8s_resource( - workload='capture-worker', - labels=['backend'], - resource_deps=['postgrest', 'postgresql-primary'], -) # k8s_resource( # workload='chihaya', # labels=['backend'] @@ -625,7 +597,12 @@ k8s_resource( labels=['database'], ) k8s_resource( - workload='migrations', + workload='migrations-schema', + labels=['database'], + resource_deps=['postgresql-primary'], +) +k8s_resource( + workload='migrations-data', labels=['database'], resource_deps=['postgresql-primary'], ) diff --git a/charts/fp/templates/migrations.yaml b/charts/fp/templates/migrations-data.yaml similarity index 65% rename from charts/fp/templates/migrations.yaml rename to charts/fp/templates/migrations-data.yaml index 4f8cf90..9cd4a9e 100644 --- a/charts/fp/templates/migrations.yaml +++ b/charts/fp/templates/migrations-data.yaml @@ -3,14 +3,14 @@ apiVersion: v1 kind: Pod metadata: - name: migrations + name: migrations-data namespace: futureporn labels: - app.kubernetes.io/name: migrations + app.kubernetes.io/name: migrations-data spec: containers: - - name: migrations - image: "{{ .Values.migrations.imageName }}" + - name: migrations-data + image: "{{ .Values.migrations.data.imageName }}" resources: {} env: - name: DATABASE_PASSWORD diff --git a/charts/fp/templates/migrations-schema.yaml b/charts/fp/templates/migrations-schema.yaml new file mode 100644 index 0000000..99f78e4 --- /dev/null +++ b/charts/fp/templates/migrations-schema.yaml @@ -0,0 +1,21 @@ + +--- +apiVersion: v1 +kind: Pod +metadata: + name: migrations-schema + namespace: futureporn + labels: + app.kubernetes.io/name: migrations-schema +spec: + containers: + - name: migrations-schema + image: "{{ .Values.migrations.schema.imageName }}" + resources: {} + env: + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: postgresql + key: password + restartPolicy: Never diff --git a/charts/fp/templates/next.yaml b/charts/fp/templates/next.yaml index 2ce9144..070613d 100644 --- a/charts/fp/templates/next.yaml +++ b/charts/fp/templates/next.yaml @@ -19,7 +19,7 @@ spec: value: "{{ .Values.uppy.url }}" - name: NEXT_PUBLIC_POSTGREST_URL value: {{ printf "https://%s" .Values.postgrest.hostname | quote }} - - name: NEXT_PUBLIC_WEBSITE_DOMAIN + - name: NEXT_PUBLIC_URL value: {{ printf "https://%s" .Values.next.hostname | quote }} - name: NEXT_PUBLIC_API_DOMAIN value: {{ .Values.next.hostname | quote }} @@ -42,13 +42,6 @@ spec: secretKeyRef: name: patreon key: clientSecret - - name: SUPERTOKENS_API_KEYS - valueFrom: - secretKeyRef: - name: supertokens - key: apiKeys - - name: SUPERTOKENS_URL - value: {{ printf "https://%s" .Values.supertokens.hostname | quote }} - name: KEYCLOAK_CLIENT_ID value: futureporn - name: KEYCLOAK_CLIENT_SECRET @@ -58,6 +51,10 @@ spec: key: clientSecret - name: KEYCLOAK_ISSUER value: {{ .Values.keycloak.issuer | quote }} + - name: KEYCLOAK_URL + value: {{ printf "https://%s" .Values.keycloak.hostname | quote }} + - name: KEYCLOAK_LOCAL_URL + value: {{ .Values.keycloak.localUrl | quote }} ports: - name: web containerPort: 3000 diff --git a/charts/fp/values.yaml b/charts/fp/values.yaml index 64cfb79..3f370e9 100644 --- a/charts/fp/values.yaml +++ b/charts/fp/values.yaml @@ -98,6 +98,7 @@ supertokens: replicas: 1 keycloak: hostname: keycloak.fp.sbtp.xyz + localUrl: http://keycloak.futureporn.svc.cluster.local:8080 replicas: 1 issuer: https://keycloak.fp.sbtp.xyz/realms/futureporn logto: @@ -111,4 +112,7 @@ whoami: hostname: whoami.fp.sbtp.xyz port: 8888 migrations: - imageName: fp/migrations + schema: + imageName: fp/migrations-schema + data: + imageName: fp/migrations-data \ No newline at end of file diff --git a/dockerfiles/migrations.dockerfile b/dockerfiles/migrations-data.dockerfile similarity index 53% rename from dockerfiles/migrations.dockerfile rename to dockerfiles/migrations-data.dockerfile index 3af7b97..8c9773e 100644 --- a/dockerfiles/migrations.dockerfile +++ b/dockerfiles/migrations-data.dockerfile @@ -6,15 +6,15 @@ RUN corepack enable && corepack prepare pnpm@9.6.0 --activate FROM base AS build COPY ./pnpm-workspace.yaml ./.npmrc . -COPY ./services/migrations/package.json ./services/migrations/pnpm-lock.yaml ./services/migrations/ +COPY ./services/migrations-data/package.json ./services/migrations-data/pnpm-lock.yaml ./services/migrations-data/ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prefer-offline -COPY ./services/migrations/ ./services/migrations/ -RUN pnpm --filter=@futureporn/migrations deploy --prod /prod/migrations -RUN ls -las /prod/migrations +COPY ./services/migrations-data/ ./services/migrations-data/ +RUN pnpm --filter=@futureporn/migrations-data deploy --prod /prod/migrations-data +RUN ls -las /prod/migrations-data -FROM base AS migrations +FROM base AS migrations-data ENV NODE_ENV=production -COPY --from=build /prod/migrations . +COPY --from=build /prod/migrations-data . ENTRYPOINT ["pnpm", "start"] diff --git a/dockerfiles/migrations-schema.dockerfile b/dockerfiles/migrations-schema.dockerfile new file mode 100644 index 0000000..106a162 --- /dev/null +++ b/dockerfiles/migrations-schema.dockerfile @@ -0,0 +1,20 @@ +FROM node:20-alpine AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@9.6.0 --activate + +FROM base AS build +COPY ./pnpm-workspace.yaml ./.npmrc . +COPY ./services/migrations-schema/package.json ./services/migrations-schema/pnpm-lock.yaml ./services/migrations-schema/ +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prefer-offline +COPY ./services/migrations-schema/ ./services/migrations-schema/ +RUN pnpm --filter=@futureporn/migrations-schema deploy --prod /prod/migrations-schema +RUN ls -las /prod/migrations-schema + +FROM base AS migrations-schema +ENV NODE_ENV=production +COPY --from=build /prod/migrations-schema . +ENTRYPOINT ["pnpm", "start"] + diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f21fe6f..db0c01c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -297,12 +297,25 @@ export interface IPlatformNotificationResponse { } -export interface IVodsResponse { - id: string -} export interface IVod { - id: string + id: number; + uuid: string; + stream?: IStream; + date: string; + date2: string; + mux_asset?: IMuxAsset; + vtuber?: IVtuber; + cuid?: string; + tag_vod_relations?: any; + video240Hash?: string; + videoSrcHash?: string; + timestamps?: any; + announce_title?: string; + announce_url?: string; + videoSrcB2?: any; + uploader: any; + note: string; } @@ -312,7 +325,7 @@ export interface IStream { date_2: string; archive_status: ArchiveStatus; vods: IVod[]; - cuid: string; + uuid: string; vtuber: IVtuber; is_chaturbate_stream: boolean; is_fansly_stream: boolean; diff --git a/scripts/data-migrations/.gitignore b/scripts/data-migrations/.gitignore deleted file mode 100644 index 92b7aae..0000000 --- a/scripts/data-migrations/.gitignore +++ /dev/null @@ -1 +0,0 @@ -2024-10-25-from-strapi-to-postgrest-mk2.sql diff --git a/scripts/data-migrations/2024-10-07-from-strapi-to-postgrest.sql b/scripts/data-migrations/2024-10-07-from-strapi-to-postgrest.sql deleted file mode 100644 index 5c122d1..0000000 --- a/scripts/data-migrations/2024-10-07-from-strapi-to-postgrest.sql +++ /dev/null @@ -1,350 +0,0 @@ -SELECT dblink_connect( - 'old_db_conn', - 'dbname=futureporn_strapi_old user=postgres passfile=/tmp/.pgpass' -); - - --- Temporary schema changes that I don't want to save in @futureporn/migrations --- 1. ADD api.s3_files.id_old --- 2. ADD api.vods.id_old --- 3. ADD api.vods_s3_join.[id_old,vod_id_old,b_2_file_id_old] --- 4. ADD api.vtubers.id_old --- 5. ADD api.vods_s3_files_joins.id_old -ALTER TABLE IF EXISTS api.s3_files - ADD COLUMN IF NOT EXISTS id_old int; - -ALTER TABLE IF EXISTS api.vods - ADD COLUMN IF NOT EXISTS id_old int; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS id_old int; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS vod_id_old int; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS b_2_file_id_old int; - -ALTER TABLE IF EXISTS api.vtubers - ADD COLUMN IF NOT EXISTS id_old int; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS id_old int; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS s3_file_id UUID; - -ALTER TABLE api.vods_s3_files_joins - ADD COLUMN IF NOT EXISTS s3_file_id_old int; - -CREATE TABLE IF NOT EXISTS api.a_temporary_vods ( - id integer, - video_src_hash character varying, - video_720_hash character varying, - video_480_hash character varying, - video_360_hash character varying, - video_240_hash character varying, - thin_hash character varying, - thicc_hash character varying, - announce_title character varying, - announce_url character varying, - note text, - date timestamp(6) without time zone, - spoilers text, - created_at timestamp(6) without time zone, - updated_at timestamp(6) without time zone, - published_at timestamp(6) without time zone, - created_by_id integer, - updated_by_id integer, - title character varying, - chat_log text, - date_2 character varying, - cuid character varying, - archive_status character varying -); - --- Enable the dblink extension --- this lets us copy data between two different databases --- in our case, we are copying tables from futureporn_strapi_old.public.streams to futureporn.api.streams -CREATE EXTENSION IF NOT EXISTS dblink; - - -SELECT dblink_connect( - 'old_db_conn', - 'dbname=futureporn_strapi_old user=postgres passfile=/tmp/.pgpass' -); - - --- Migrate vtubers table -INSERT INTO api.vtubers ( - id, - id_old, - chaturbate, - twitter, - patreon, - twitch, - tiktok, - onlyfans, - youtube, - linktree, - carrd, - fansly, - pornhub, - discord, - reddit, - throne, - instagram, - facebook, - merch, - slug, - image, - display_name, - description1, - description2, - created_at, - updated_at, - theme_color, - image_blur -) -SELECT DISTINCT - gen_random_uuid() AS id, - v.id AS id_old, - v.chaturbate, - v.twitter, - v.patreon, - v.twitch, - v.tiktok, - v.onlyfans, - v.youtube, - v.linktree, - v.carrd, - v.fansly, - v.pornhub, - v.discord, - v.reddit, - v.throne, - v.instagram, - v.facebook, - v.merch, - v.slug, - v.image, - v.display_name, - v.description_1, - v.description_2, - v.created_at, - v.updated_at, - v.theme_color, - v.image_blur -FROM dblink('old_db_conn', 'SELECT id, - chaturbate, - twitter, - patreon, - twitch, - tiktok, - onlyfans, - youtube, - linktree, - carrd, - fansly, - pornhub, - discord, - reddit, - throne, - instagram, - facebook, - merch, - slug, - image, - display_name, - description_1, - description_2, - created_at, - updated_at, - published_at, - created_by_id, - updated_by_id, - theme_color, - image_blur - FROM public.vtubers') -AS v( - id integer, - chaturbate character varying(255), - twitter character varying(255), - patreon character varying(255), - twitch character varying(255), - tiktok character varying(255), - onlyfans character varying(255), - youtube character varying(255), - linktree character varying(255), - carrd character varying(255), - fansly character varying(255), - pornhub character varying(255), - discord character varying(255), - reddit character varying(255), - throne character varying(255), - instagram character varying(255), - facebook character varying(255), - merch character varying(255), - slug character varying(255), - image character varying(255), - display_name character varying(255), - description_1 text, - description_2 text, - created_at timestamp(6) without time zone, - updated_at timestamp(6) without time zone, - published_at timestamp(6) without time zone, - created_by_id integer, - updated_by_id integer, - theme_color character varying(255), - image_blur character varying(255) -); - - - --- Migrate streams table --- here we are taking the pre-existing data from the strapi database --- and copying it to the postgrest database. --- some of the columns like vtuber need to be set to NULL because they are new and strapi streams table didn't contain that info -INSERT INTO api.streams (platform_notification_type, date, vtuber, tweet, archive_status, is_chaturbate_stream, is_fansly_stream) -SELECT DISTINCT - NULL AS platform_notification_type, - s.date, - NULL::UUID AS vtuber, - NULL AS tweet, - s.archive_status, - s.is_chaturbate_stream, - s.is_fansly_stream -FROM dblink('old_db_conn', 'SELECT date, archive_status, is_chaturbate_stream, is_fansly_stream FROM public.streams') -AS s( - date timestamp, - archive_status character varying, - is_chaturbate_stream boolean, - is_fansly_stream boolean -); - - --- Migrate vods b2_files join table --- previously public.vods_video_src_b_2_links --- new api.vods_s3_join -INSERT INTO api.vods_s3_files_joins (id, id_old, vod_id, vod_id_old, s3_file_id, s3_file_id_old) -SELECT DISTINCT - gen_random_uuid() AS id, - old.id AS id_old, - NULL::UUID AS vod_id, - old.vod_id AS vod_id_old, - NULL::UUID AS s3_file_id, - old.b_2_file_id AS s3_file_id_old -FROM dblink('old_db_conn', 'SELECT id, vod_id, b_2_file_id FROM public.vods_video_src_b_2_links') -AS old( - id int, - vod_id int, - b_2_file_id int -); - - - - --- Migrate B2 table -INSERT INTO api.s3_files ( - id, - id_old, - s3_id, - s3_key, - created_at, - updated_at, - bucket, - cdn_url -) -SELECT - gen_random_uuid()::UUID AS id, - b2_file.id::INT AS id_old, - b2_file.upload_id::TEXT AS s3_id, - b2_file.key::TEXT AS s3_key, - b2_file.created_at::TIMESTAMP(6) WITHOUT TIME ZONE AS created_at, - b2_file.updated_at::TIMESTAMP(6) WITHOUT TIME ZONE AS updated_at, - 'futureporn-b2'::TEXT AS bucket, - b2_file.cdn_url::TEXT AS cdn_url -FROM - dblink('old_db_conn', 'SELECT id, key, upload_id, created_at, updated_at, cdn_url FROM public.b2_files') AS b2_file ( - id integer, - key character varying(255), - upload_id character varying(255), - created_at timestamp(6) without time zone, - updated_at timestamp(6) without time zone, - cdn_url character varying(255) - ); - - --- Migrate vods table -INSERT INTO api.vods ( - id, - id_old, - stream_id, - created_at, - updated_at, - title, - date, - note, - ipfs_cid, - s3_file, - announce_title, - announce_url, - status -) -SELECT - gen_random_uuid(), - vods.id, - NULL, - vods.created_at, - vods.updated_at, - vods.title, - vods.date::date, - vods.note, - vods.video_src_hash, - NULL, -- old vods doesn't contain this info-- the join table is needed - vods.announce_title, - vods.announce_url, - 'pending_recording' -FROM - dblink('old_db_conn', 'SELECT * FROM public.vods') AS vods ( - id integer, - video_src_hash character varying, - video_720_hash character varying, - video_480_hash character varying, - video_360_hash character varying, - video_240_hash character varying, - thin_hash character varying, - thicc_hash character varying, - announce_title character varying, - announce_url character varying, - note text, - date timestamp(6) without time zone, - spoilers text, - created_at timestamp(6) without time zone, - updated_at timestamp(6) without time zone, - published_at timestamp(6) without time zone, - created_by_id integer, - updated_by_id integer, - title character varying, - chat_log text, - date_2 character varying, - cuid character varying, - archive_status character varying - ) - LEFT JOIN ( - -- Fetching vods_vtuber_links from the old database - SELECT * - FROM dblink('old_db_conn', 'SELECT vod_id, vtuber_id FROM public.vods_vtuber_links') AS links ( - vod_id integer, - vtuber_id integer - ) - ) AS links ON vods.id = links.vod_id - LEFT JOIN api.vtubers AS vtubers - ON links.vtuber_id = vtubers.id_old; -- Map the old `vtuber_id` to the new `uuid` in `vtubers` - - - --- Now we copy patron data from the old Strapi table up_user --- Going forward we are changing how Patrons table is populated. - --- FROM up_user diff --git a/scripts/data-migrations/a.migration.sh b/scripts/data-migrations/a.migration.sh deleted file mode 100755 index ba4c8ce..0000000 --- a/scripts/data-migrations/a.migration.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -postgres_pod_name=postgresql-primary-0 - -if [ -z $POSTGRES_PASSWORD ]; then - echo "POSTGRES_PASSWORD was missing in env. Please run using dotenvx or similar" - exit 5 -fi - - -if [ -z "$1" ] - then - echo "Usage: a.migration.sh /path/to/migraiton.sql" - exit 6 -fi - -echo "create .pgpass file inside pod" -kubectl -n futureporn exec -i ${postgres_pod_name} -- bash -c "echo *:5432:*:postgres:${POSTGRES_PASSWORD} | tee /tmp/.pgpass" -kubectl -n futureporn exec -i ${postgres_pod_name} -- chmod 0600 /tmp/.pgpass - -echo "Copying sql to pod" -kubectl -n futureporn cp ${1} ${postgres_pod_name}:/tmp/migration.sql - -echo "Running ${1} inside the pod" -kubectl -n futureporn exec -i ${postgres_pod_name} -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres -d futureporn -f /tmp/migration.sql - -echo "rm .pgpass file" -kubectl -n futureporn exec -i ${postgres_pod_name} -- rm -rf /tmp/.pgpass \ No newline at end of file diff --git a/scripts/data-migrations/scratch.sql b/scripts/data-migrations/scratch.sql deleted file mode 100644 index b71797c..0000000 --- a/scripts/data-migrations/scratch.sql +++ /dev/null @@ -1,14 +0,0 @@ - - SELECT - gen_random_uuid() AS id, - vods.id AS id_old, - links.vod_id AS vod_id_old, - links.vtuber_id AS vtuber, - stream_links.stream_id AS stream - - FROM public_strapi_old.vods AS vods - LEFT JOIN public_strapi_old.vods_vtuber_links AS links - ON vods.id = links.vod_id - - LEFT JOIN public_strapi_old.vods_stream_links AS stream_links - ON vods.id = stream_links.vod_id \ No newline at end of file diff --git a/services/factory/package.json b/services/factory/package.json index da7b3c9..b6c360e 100644 --- a/services/factory/package.json +++ b/services/factory/package.json @@ -8,7 +8,7 @@ "test": "mocha", "dev": "pnpm run dev.nodemon # yes this is crazy to have nodemon execute tsx, but it's the only way I have found to get live reloading in TS/ESM/docker with Graphile Worker's way of loading tasks", "dev.tsx": "tsx ./src/index.ts", - "dev.nodemon": "nodemon --ext ts --exec \"pnpm run dev.tsx\"", + "dev.nodemon": "nodemon --exitcrash --ext ts --exec \"pnpm run dev.tsx\"", "dev.node": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/index.ts" }, "keywords": [ diff --git a/services/migrations-data/README.md b/services/migrations-data/README.md new file mode 100644 index 0000000..094ae89 --- /dev/null +++ b/services/migrations-data/README.md @@ -0,0 +1,27 @@ +# @futureporn/migrations-data + +Here we handle data migrations for the postgrest database. + +@see https://github.com/zakpatterson/postgres-schema-migrations + +Reminder: only write migrations that affect data. (don't write migrations that affect schema) + +## K.I.S.S. + +Keep It Stupidly Simple. + +We are keeping this module as simple as possible. This means pure JS (no typescript!) + + +## troubleshooting + +If you see the following error, graphile_worker likely hasn't had a chance to create it's functions. Make sure that a graphile_worker is running, so it can automatically create the necessary functions. + +```json +{ + "code": "42883", + "details": null, + "hint": "No function matches the given name and argument types. You might need to add explicit type casts.", + "message": "function graphile_worker.add_job(text, json, max_attempts => integer) does not exist" +} +``` \ No newline at end of file diff --git a/services/migrations-data/index.js b/services/migrations-data/index.js new file mode 100644 index 0000000..2b8ac31 --- /dev/null +++ b/services/migrations-data/index.js @@ -0,0 +1,68 @@ +import pg from 'pg' +import { migrate } from 'postgres-schema-migrations'; +import path, { dirname } from 'node:path'; +import { fileURLToPath } from 'url'; +import 'dotenv/config'; + +const { Client } = pg +const __dirname = dirname(fileURLToPath(import.meta.url)); + +if (!process.env.DATABASE_PASSWORD) throw new Error('DATABASE_PASSWORD is missing in env'); + +/* + * Here we set up a Foreign Data Wrapper which connects us to the old strapi database. + * From this Strapi db futureporn-old, we migrate data to futureporn database. + */ + +async function setupForeignDataWrapper(client) { + + + + // Run the SQL commands + const sql = ` + BEGIN; + + CREATE EXTENSION IF NOT EXISTS postgres_fdw; + + CREATE SERVER IF NOT EXISTS futureporn_old + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname 'futureporn_old'); + + CREATE USER MAPPING IF NOT EXISTS FOR postgres + SERVER futureporn_old + OPTIONS (password_required 'true', password '${process.env.DATABASE_PASSWORD}'); + + COMMIT; + `; + + await client.query(sql); + console.log('Foreign Data Wrapper setup completed successfully.'); + +} + +async function main() { + const dbConfig = { + database: "futureporn", + user: "postgres", + password: process.env.DATABASE_PASSWORD, + host: 'postgresql-primary.futureporn.svc.cluster.local', + port: 5432, + } + const client = new Client(dbConfig) + await client.connect() + const migrateConfig = { + client, + ensureDatabaseExists: false, + defaultDatabase: 'postgres' + } + + try { + await setupForeignDataWrapper(client) + await migrate(migrateConfig, path.join(__dirname, "./migrations/"), { logger: console.log, schema: 'migrations_data' }) + } finally { + await client.end() + } +} + + +await main() \ No newline at end of file diff --git a/scripts/data-migrations/2024-10-25-from-strapi-to-postgrest-mk2.sql b/services/migrations-data/migrations/0001_from-strapi-to-postgrest-mk2.sql similarity index 97% rename from scripts/data-migrations/2024-10-25-from-strapi-to-postgrest-mk2.sql rename to services/migrations-data/migrations/0001_from-strapi-to-postgrest-mk2.sql index 6d99747..9ae1d1c 100644 --- a/scripts/data-migrations/2024-10-25-from-strapi-to-postgrest-mk2.sql +++ b/services/migrations-data/migrations/0001_from-strapi-to-postgrest-mk2.sql @@ -1,17 +1,4 @@ - -BEGIN; - -CREATE EXTENSION IF NOT EXISTS postgres_fdw; - -CREATE SERVER IF NOT EXISTS futureporn_old - FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname 'futureporn_old'); - -CREATE USER MAPPING IF NOT EXISTS FOR postgres - SERVER futureporn_old - OPTIONS (password_required 'true', password ''); -- @todo add password here - - +-- 2024-10-25 /** * @@ -58,6 +45,8 @@ CREATE USER MAPPING IF NOT EXISTS FOR postgres * */ +SET search_path TO 'public'; + CREATE FOREIGN TABLE external_b2_files ( id INT, @@ -274,6 +263,7 @@ CREATE FOREIGN TABLE external_vods published_at date, title text, date date NOT NULL, + date_2 timestamp without time zone, mux_asset INT, thumbnail INT, vtuber INT, @@ -584,6 +574,7 @@ INSERT INTO api.streams ( id, platform_notification_type, date, + date_2, created_at, updated_at, vtuber_num, @@ -595,7 +586,8 @@ OVERRIDING SYSTEM VALUE SELECT streams.id, NULL AS platform_notification_type, -- Modify if necessary - streams.date_2::TIMESTAMP WITHOUT TIME ZONE AS date, + streams.date_2::TIMESTAMP WITH TIME ZONE AS date, + streams.date_2::TIMESTAMP WITH TIME ZONE AS date_2, streams.created_at, streams.updated_at, links.vtuber_id AS vtuber_num, @@ -630,7 +622,8 @@ INSERT INTO api.vods ( updated_at, published_at, title, - date, + date, + date_2, note, ipfs_cid, announce_title, @@ -644,7 +637,8 @@ SELECT vods.updated_at, vods.published_at, vods.title, - vods.date::date, + vods.date::TIMESTAMP WITH TIME ZONE, + vods.date_2::TIMESTAMP WITH TIME ZONE AS date_2, vods.note, vods.video_src_hash AS ipfs_cid, vods.announce_title, @@ -984,5 +978,3 @@ FROM public.external_vtubers_toys_links; - -COMMIT; \ No newline at end of file diff --git a/scripts/data-migrations/2024-11-21-relate-vtubers-to-vods.sql b/services/migrations-data/migrations/0002_relate-vtubers-to-vods.sql similarity index 96% rename from scripts/data-migrations/2024-11-21-relate-vtubers-to-vods.sql rename to services/migrations-data/migrations/0002_relate-vtubers-to-vods.sql index 95d5ff2..801e0e8 100644 --- a/scripts/data-migrations/2024-11-21-relate-vtubers-to-vods.sql +++ b/services/migrations-data/migrations/0002_relate-vtubers-to-vods.sql @@ -1,5 +1,4 @@ - -BEGIN; +-- 2024-11-21 -- SELECT * FROM api.vods AS vods -- INNER JOIN api.vods_vtuber_links AS links @@ -16,4 +15,3 @@ FROM api.vods_vtuber_links links WHERE api.vods.id = links.vod_id; -COMMIT; \ No newline at end of file diff --git a/scripts/data-migrations/2024-11-22-relate-mux_asset_id-to-vods.sql b/services/migrations-data/migrations/0003_relate-mux_asset_id-to-vods.sql similarity index 88% rename from scripts/data-migrations/2024-11-22-relate-mux_asset_id-to-vods.sql rename to services/migrations-data/migrations/0003_relate-mux_asset_id-to-vods.sql index 54b2365..c118eba 100644 --- a/scripts/data-migrations/2024-11-22-relate-mux_asset_id-to-vods.sql +++ b/services/migrations-data/migrations/0003_relate-mux_asset_id-to-vods.sql @@ -1,11 +1,9 @@ - -BEGIN; +-- 2024-11-22 UPDATE api.vods SET mux_asset_id = links.mux_asset_id FROM api.vods_mux_asset_links links WHERE api.vods.id = links.vod_id; -COMMIT; diff --git a/scripts/data-migrations/2024-11-22-relate-thumbnails-to-vods.sql b/services/migrations-data/migrations/0004_relate-thumbnails-to-vods.sql similarity index 88% rename from scripts/data-migrations/2024-11-22-relate-thumbnails-to-vods.sql rename to services/migrations-data/migrations/0004_relate-thumbnails-to-vods.sql index 587c372..d812c90 100644 --- a/scripts/data-migrations/2024-11-22-relate-thumbnails-to-vods.sql +++ b/services/migrations-data/migrations/0004_relate-thumbnails-to-vods.sql @@ -1,11 +1,9 @@ - -BEGIN; +-- 2024-11-22 UPDATE api.vods SET thumbnail_id = links.b_2_file_id FROM api.vods_thumbnail_links links WHERE api.vods.id = links.vod_id; -COMMIT; diff --git a/scripts/data-migrations/2024-11-22-rename-vtuber_num-to-vtuber_id.sql b/services/migrations-data/migrations/0005_rename-vtuber_num-to-vtuber_id.sql similarity index 82% rename from scripts/data-migrations/2024-11-22-rename-vtuber_num-to-vtuber_id.sql rename to services/migrations-data/migrations/0005_rename-vtuber_num-to-vtuber_id.sql index f4e2019..cefe465 100644 --- a/scripts/data-migrations/2024-11-22-rename-vtuber_num-to-vtuber_id.sql +++ b/services/migrations-data/migrations/0005_rename-vtuber_num-to-vtuber_id.sql @@ -1,11 +1,9 @@ - -BEGIN; +-- 2024-11-22 UPDATE api.streams SET vtuber_id = vtuber_num WHERE vtuber_num IS NOT NULL; -COMMIT; -- @TODO api.streams.vtuber_num is deprecated in favor of api.streams.vtuber_Id \ No newline at end of file diff --git a/services/migrations-data/migrations/0006_relate-vods-to-streams.sql b/services/migrations-data/migrations/0006_relate-vods-to-streams.sql new file mode 100644 index 0000000..e9a7695 --- /dev/null +++ b/services/migrations-data/migrations/0006_relate-vods-to-streams.sql @@ -0,0 +1,9 @@ +-- 2024-12-16 + +-- Relate VODs to streams by matching the same date + +-- Update existing VODs to associate them with the corresponding stream +UPDATE api.vods +SET stream_id = streams.id +FROM api.streams +WHERE vods.date = streams.date; \ No newline at end of file diff --git a/scripts/data-migrations/README.md b/services/migrations-data/migrations/README.md similarity index 87% rename from scripts/data-migrations/README.md rename to services/migrations-data/migrations/README.md index af966ab..1d6b1ca 100644 --- a/scripts/data-migrations/README.md +++ b/services/migrations-data/migrations/README.md @@ -1,7 +1,7 @@ # Futureporn data migrations This directory is for data migrations ONLY. -For schema migrations, see ../services/migrations node package +For schema migrations, see ./migrations node package ## Usage diff --git a/services/migrations-data/package.json b/services/migrations-data/package.json new file mode 100644 index 0000000..3925948 --- /dev/null +++ b/services/migrations-data/package.json @@ -0,0 +1,20 @@ +{ + "name": "@futureporn/migrations-data", + "type": "module", + "version": "0.6.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 0", + "start": "node index.js" + }, + "packageManager": "pnpm@9.6.0", + "keywords": [], + "author": "@CJ_Clippy", + "license": "Unlicense", + "dependencies": { + "dotenv": "^16.4.5", + "pg": "8.12.0", + "postgres-schema-migrations": "^6.1.0" + } +} diff --git a/services/migrations-data/pnpm-lock.yaml b/services/migrations-data/pnpm-lock.yaml new file mode 100644 index 0000000..3fe9609 --- /dev/null +++ b/services/migrations-data/pnpm-lock.yaml @@ -0,0 +1,186 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + pg: + specifier: 8.12.0 + version: 8.12.0 + postgres-schema-migrations: + specifier: ^6.1.0 + version: 6.1.0 + + ../..: {} + + ../../packages/fetchers: {} + + ../../packages/infra: {} + + ../../packages/storage: {} + + ../../packages/types: {} + + ../../packages/utils: {} + + ../bot: {} + + ../capture: {} + + ../factory: {} + + ../htmx: {} + + ../mailbox: {} + + ../migrations-schema: {} + + ../next: {} + + ../scout: {} + + ../strapi: {} + + ../uppy: {} + +packages: + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.6.4: + resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.6.2: + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.12.0: + resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-schema-migrations@6.1.0: + resolution: {integrity: sha512-d1LJ+A9Lg4kAwuh91S8ozF8q3adFNJlStbpUF/sbjMTzSIzJClpmg4D6qyd9nvKt2el0rnZJjXZQ2r01Y5OpzA==} + engines: {node: '>10.17.0'} + hasBin: true + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sql-template-strings@2.2.2: + resolution: {integrity: sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==} + engines: {node: '>=4.0.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + +snapshots: + + dotenv@16.4.5: {} + + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.6.4: {} + + pg-int8@1.0.1: {} + + pg-pool@3.6.2(pg@8.12.0): + dependencies: + pg: 8.12.0 + + pg-protocol@1.6.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.12.0: + dependencies: + pg-connection-string: 2.6.4 + pg-pool: 3.6.2(pg@8.12.0) + pg-protocol: 1.6.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-schema-migrations@6.1.0: + dependencies: + pg: 8.12.0 + sql-template-strings: 2.2.2 + transitivePeerDependencies: + - pg-native + + split2@4.2.0: {} + + sql-template-strings@2.2.2: {} + + xtend@4.0.2: {} diff --git a/services/migrations/README.md b/services/migrations-schema/README.md similarity index 92% rename from services/migrations/README.md rename to services/migrations-schema/README.md index b664380..4fdd8da 100644 --- a/services/migrations/README.md +++ b/services/migrations-schema/README.md @@ -2,7 +2,7 @@ Here we handle migrations for the postgrest database. -@see https://github.com/thomwright/postgres-migrations +@see https://github.com/zakpatterson/postgres-schema-migrations Reminder: only write migrations that affect schema. (don't write migrations that affect data) diff --git a/services/migrations/index.js b/services/migrations-schema/index.js similarity index 86% rename from services/migrations/index.js rename to services/migrations-schema/index.js index a80c68e..435b172 100644 --- a/services/migrations/index.js +++ b/services/migrations-schema/index.js @@ -1,4 +1,4 @@ -import {migrate} from 'postgres-migrations' +import {migrate} from 'postgres-schema-migrations' import path, { dirname } from 'node:path' import { fileURLToPath } from 'url'; import 'dotenv/config' @@ -25,7 +25,7 @@ async function main() { defaultDatabase: "postgres" } - await migrate(dbConfig, path.join(__dirname, "./migrations/"), { logger: console.log }) + await migrate(dbConfig, path.join(__dirname, "./migrations/"), { schema: 'migrations_schema', logger: console.log }) } diff --git a/services/migrations/migrations/00001_create.sql b/services/migrations-schema/migrations/00001_create.sql similarity index 100% rename from services/migrations/migrations/00001_create.sql rename to services/migrations-schema/migrations/00001_create.sql diff --git a/services/migrations/migrations/00002_add-records-table.sql b/services/migrations-schema/migrations/00002_add-records-table.sql similarity index 100% rename from services/migrations/migrations/00002_add-records-table.sql rename to services/migrations-schema/migrations/00002_add-records-table.sql diff --git a/services/migrations/migrations/00003_create-graphile-worker-schema.sql b/services/migrations-schema/migrations/00003_create-graphile-worker-schema.sql similarity index 100% rename from services/migrations/migrations/00003_create-graphile-worker-schema.sql rename to services/migrations-schema/migrations/00003_create-graphile-worker-schema.sql diff --git a/services/migrations/migrations/00004_create-trigger-function.sql b/services/migrations-schema/migrations/00004_create-trigger-function.sql similarity index 100% rename from services/migrations/migrations/00004_create-trigger-function.sql rename to services/migrations-schema/migrations/00004_create-trigger-function.sql diff --git a/services/migrations/migrations/00005_add-trigger-for-record-update.sql b/services/migrations-schema/migrations/00005_add-trigger-for-record-update.sql similarity index 100% rename from services/migrations/migrations/00005_add-trigger-for-record-update.sql rename to services/migrations-schema/migrations/00005_add-trigger-for-record-update.sql diff --git a/services/migrations/migrations/00006_add-updated-at-to-records.sql b/services/migrations-schema/migrations/00006_add-updated-at-to-records.sql similarity index 100% rename from services/migrations/migrations/00006_add-updated-at-to-records.sql rename to services/migrations-schema/migrations/00006_add-updated-at-to-records.sql diff --git a/services/migrations/migrations/00007_add-default-records-timestamps.sql b/services/migrations-schema/migrations/00007_add-default-records-timestamps.sql similarity index 100% rename from services/migrations/migrations/00007_add-default-records-timestamps.sql rename to services/migrations-schema/migrations/00007_add-default-records-timestamps.sql diff --git a/services/migrations/migrations/00008_add-default-records-timestamp.sql b/services/migrations-schema/migrations/00008_add-default-records-timestamp.sql similarity index 100% rename from services/migrations/migrations/00008_add-default-records-timestamp.sql rename to services/migrations-schema/migrations/00008_add-default-records-timestamp.sql diff --git a/services/migrations/migrations/00009_add-streams-vods-vtubers.sql b/services/migrations-schema/migrations/00009_add-streams-vods-vtubers.sql similarity index 100% rename from services/migrations/migrations/00009_add-streams-vods-vtubers.sql rename to services/migrations-schema/migrations/00009_add-streams-vods-vtubers.sql diff --git a/services/migrations/migrations/00010_record-segments.sql b/services/migrations-schema/migrations/00010_record-segments.sql similarity index 100% rename from services/migrations/migrations/00010_record-segments.sql rename to services/migrations-schema/migrations/00010_record-segments.sql diff --git a/services/migrations/migrations/00011_use-composite-primary-keys.sql b/services/migrations-schema/migrations/00011_use-composite-primary-keys.sql similarity index 100% rename from services/migrations/migrations/00011_use-composite-primary-keys.sql rename to services/migrations-schema/migrations/00011_use-composite-primary-keys.sql diff --git a/services/migrations/migrations/00012_order-records_segments.sql b/services/migrations-schema/migrations/00012_order-records_segments.sql similarity index 100% rename from services/migrations/migrations/00012_order-records_segments.sql rename to services/migrations-schema/migrations/00012_order-records_segments.sql diff --git a/services/migrations/migrations/00013_rename-segments-order.sql b/services/migrations-schema/migrations/00013_rename-segments-order.sql similarity index 100% rename from services/migrations/migrations/00013_rename-segments-order.sql rename to services/migrations-schema/migrations/00013_rename-segments-order.sql diff --git a/services/migrations/migrations/00014_create-segments-stream-links.sql b/services/migrations-schema/migrations/00014_create-segments-stream-links.sql similarity index 100% rename from services/migrations/migrations/00014_create-segments-stream-links.sql rename to services/migrations-schema/migrations/00014_create-segments-stream-links.sql diff --git a/services/migrations/migrations/00015_create-segments-stream-links-2.sql b/services/migrations-schema/migrations/00015_create-segments-stream-links-2.sql similarity index 100% rename from services/migrations/migrations/00015_create-segments-stream-links-2.sql rename to services/migrations-schema/migrations/00015_create-segments-stream-links-2.sql diff --git a/services/migrations/migrations/00016_remove-unecessary-columns.sql b/services/migrations-schema/migrations/00016_remove-unecessary-columns.sql similarity index 100% rename from services/migrations/migrations/00016_remove-unecessary-columns.sql rename to services/migrations-schema/migrations/00016_remove-unecessary-columns.sql diff --git a/services/migrations/migrations/00017_add-stream-status-col.sql b/services/migrations-schema/migrations/00017_add-stream-status-col.sql similarity index 100% rename from services/migrations/migrations/00017_add-stream-status-col.sql rename to services/migrations-schema/migrations/00017_add-stream-status-col.sql diff --git a/services/migrations/migrations/00018_add-stream-status-default.sql b/services/migrations-schema/migrations/00018_add-stream-status-default.sql similarity index 100% rename from services/migrations/migrations/00018_add-stream-status-default.sql rename to services/migrations-schema/migrations/00018_add-stream-status-default.sql diff --git a/services/migrations/migrations/00019_drop-discord-interactions.sql b/services/migrations-schema/migrations/00019_drop-discord-interactions.sql similarity index 100% rename from services/migrations/migrations/00019_drop-discord-interactions.sql rename to services/migrations-schema/migrations/00019_drop-discord-interactions.sql diff --git a/services/migrations/migrations/00020_add-streams-update-trigger.sql b/services/migrations-schema/migrations/00020_add-streams-update-trigger.sql similarity index 100% rename from services/migrations/migrations/00020_add-streams-update-trigger.sql rename to services/migrations-schema/migrations/00020_add-streams-update-trigger.sql diff --git a/services/migrations/migrations/00021_add-foreign-key-to-segments_stream.sql b/services/migrations-schema/migrations/00021_add-foreign-key-to-segments_stream.sql similarity index 100% rename from services/migrations/migrations/00021_add-foreign-key-to-segments_stream.sql rename to services/migrations-schema/migrations/00021_add-foreign-key-to-segments_stream.sql diff --git a/services/migrations/migrations/00022_add-permissions-for-segments_stream_links.sql b/services/migrations-schema/migrations/00022_add-permissions-for-segments_stream_links.sql similarity index 100% rename from services/migrations/migrations/00022_add-permissions-for-segments_stream_links.sql rename to services/migrations-schema/migrations/00022_add-permissions-for-segments_stream_links.sql diff --git a/services/migrations/migrations/00023_drop-capture_job_id-column.sql b/services/migrations-schema/migrations/00023_drop-capture_job_id-column.sql similarity index 100% rename from services/migrations/migrations/00023_drop-capture_job_id-column.sql rename to services/migrations-schema/migrations/00023_drop-capture_job_id-column.sql diff --git a/services/migrations/migrations/00024_add-updated_at-for-segments.sql b/services/migrations-schema/migrations/00024_add-updated_at-for-segments.sql similarity index 100% rename from services/migrations/migrations/00024_add-updated_at-for-segments.sql rename to services/migrations-schema/migrations/00024_add-updated_at-for-segments.sql diff --git a/services/migrations/migrations/00025_add-is_recording_aborted-to-streams.sql b/services/migrations-schema/migrations/00025_add-is_recording_aborted-to-streams.sql similarity index 100% rename from services/migrations/migrations/00025_add-is_recording_aborted-to-streams.sql rename to services/migrations-schema/migrations/00025_add-is_recording_aborted-to-streams.sql diff --git a/services/migrations/migrations/00026_use-moddatetime.sql b/services/migrations-schema/migrations/00026_use-moddatetime.sql similarity index 100% rename from services/migrations/migrations/00026_use-moddatetime.sql rename to services/migrations-schema/migrations/00026_use-moddatetime.sql diff --git a/services/migrations/migrations/00027_create-triggers-for-moddatetime.sql b/services/migrations-schema/migrations/00027_create-triggers-for-moddatetime.sql similarity index 100% rename from services/migrations/migrations/00027_create-triggers-for-moddatetime.sql rename to services/migrations-schema/migrations/00027_create-triggers-for-moddatetime.sql diff --git a/services/migrations/migrations/00028_remove-moddate-on-insert.sql b/services/migrations-schema/migrations/00028_remove-moddate-on-insert.sql similarity index 100% rename from services/migrations/migrations/00028_remove-moddate-on-insert.sql rename to services/migrations-schema/migrations/00028_remove-moddate-on-insert.sql diff --git a/services/migrations/migrations/00029_add-discord-message-id.sql b/services/migrations-schema/migrations/00029_add-discord-message-id.sql similarity index 100% rename from services/migrations/migrations/00029_add-discord-message-id.sql rename to services/migrations-schema/migrations/00029_add-discord-message-id.sql diff --git a/services/migrations/migrations/00030_update-update_discord_message.sql b/services/migrations-schema/migrations/00030_update-update_discord_message.sql similarity index 100% rename from services/migrations/migrations/00030_update-update_discord_message.sql rename to services/migrations-schema/migrations/00030_update-update_discord_message.sql diff --git a/services/migrations/migrations/00031_recreate-update_stream-trigger.sql b/services/migrations-schema/migrations/00031_recreate-update_stream-trigger.sql similarity index 100% rename from services/migrations/migrations/00031_recreate-update_stream-trigger.sql rename to services/migrations-schema/migrations/00031_recreate-update_stream-trigger.sql diff --git a/services/migrations/migrations/00032_update-stream-when-segment-updates.sql b/services/migrations-schema/migrations/00032_update-stream-when-segment-updates.sql similarity index 100% rename from services/migrations/migrations/00032_update-stream-when-segment-updates.sql rename to services/migrations-schema/migrations/00032_update-stream-when-segment-updates.sql diff --git a/services/migrations/migrations/00033_create-vods.sql b/services/migrations-schema/migrations/00033_create-vods.sql similarity index 100% rename from services/migrations/migrations/00033_create-vods.sql rename to services/migrations-schema/migrations/00033_create-vods.sql diff --git a/services/migrations/migrations/00034_move-segments-to-vods.sql b/services/migrations-schema/migrations/00034_move-segments-to-vods.sql similarity index 100% rename from services/migrations/migrations/00034_move-segments-to-vods.sql rename to services/migrations-schema/migrations/00034_move-segments-to-vods.sql diff --git a/services/migrations/migrations/00035_seed-vtubers.sql b/services/migrations-schema/migrations/00035_seed-vtubers.sql similarity index 100% rename from services/migrations/migrations/00035_seed-vtubers.sql rename to services/migrations-schema/migrations/00035_seed-vtubers.sql diff --git a/services/migrations/migrations/00036_update_discord_message-use-vod_id.sql b/services/migrations-schema/migrations/00036_update_discord_message-use-vod_id.sql similarity index 100% rename from services/migrations/migrations/00036_update_discord_message-use-vod_id.sql rename to services/migrations-schema/migrations/00036_update_discord_message-use-vod_id.sql diff --git a/services/migrations/migrations/00037_drop-oudated-streamscolumns.sql b/services/migrations-schema/migrations/00037_drop-oudated-streamscolumns.sql similarity index 100% rename from services/migrations/migrations/00037_drop-oudated-streamscolumns.sql rename to services/migrations-schema/migrations/00037_drop-oudated-streamscolumns.sql diff --git a/services/migrations/migrations/00038_add-vods-update-trigger.sql b/services/migrations-schema/migrations/00038_add-vods-update-trigger.sql similarity index 100% rename from services/migrations/migrations/00038_add-vods-update-trigger.sql rename to services/migrations-schema/migrations/00038_add-vods-update-trigger.sql diff --git a/services/migrations/migrations/00039_create-limiter-table.sql b/services/migrations-schema/migrations/00039_create-limiter-table.sql similarity index 100% rename from services/migrations/migrations/00039_create-limiter-table.sql rename to services/migrations-schema/migrations/00039_create-limiter-table.sql diff --git a/services/migrations/migrations/00040_theme_color-is-optional.sql b/services/migrations-schema/migrations/00040_theme_color-is-optional.sql similarity index 100% rename from services/migrations/migrations/00040_theme_color-is-optional.sql rename to services/migrations-schema/migrations/00040_theme_color-is-optional.sql diff --git a/services/migrations/migrations/00041_add-is_recording_aborted-to-vods.sql b/services/migrations-schema/migrations/00041_add-is_recording_aborted-to-vods.sql similarity index 100% rename from services/migrations/migrations/00041_add-is_recording_aborted-to-vods.sql rename to services/migrations-schema/migrations/00041_add-is_recording_aborted-to-vods.sql diff --git a/services/migrations/migrations/00042_segments-vods-fk.sql b/services/migrations-schema/migrations/00042_segments-vods-fk.sql similarity index 100% rename from services/migrations/migrations/00042_segments-vods-fk.sql rename to services/migrations-schema/migrations/00042_segments-vods-fk.sql diff --git a/services/migrations/migrations/00043_update_vod_on_segment_update.sql b/services/migrations-schema/migrations/00043_update_vod_on_segment_update.sql similarity index 100% rename from services/migrations/migrations/00043_update_vod_on_segment_update.sql rename to services/migrations-schema/migrations/00043_update_vod_on_segment_update.sql diff --git a/services/migrations/migrations/00044_segments-uuid.sql b/services/migrations-schema/migrations/00044_segments-uuid.sql similarity index 100% rename from services/migrations/migrations/00044_segments-uuid.sql rename to services/migrations-schema/migrations/00044_segments-uuid.sql diff --git a/services/migrations/migrations/00045_recreate-segments_vod_links.sql b/services/migrations-schema/migrations/00045_recreate-segments_vod_links.sql similarity index 100% rename from services/migrations/migrations/00045_recreate-segments_vod_links.sql rename to services/migrations-schema/migrations/00045_recreate-segments_vod_links.sql diff --git a/services/migrations/migrations/00046_segments_vod_links-many-to-one.sql b/services/migrations-schema/migrations/00046_segments_vod_links-many-to-one.sql similarity index 100% rename from services/migrations/migrations/00046_segments_vod_links-many-to-one.sql rename to services/migrations-schema/migrations/00046_segments_vod_links-many-to-one.sql diff --git a/services/migrations/migrations/00047_add-is_recording_aborted-to-vods-default-false.sql b/services/migrations-schema/migrations/00047_add-is_recording_aborted-to-vods-default-false.sql similarity index 100% rename from services/migrations/migrations/00047_add-is_recording_aborted-to-vods-default-false.sql rename to services/migrations-schema/migrations/00047_add-is_recording_aborted-to-vods-default-false.sql diff --git a/services/migrations/migrations/00048_segments-vod-links-perms.sql b/services/migrations-schema/migrations/00048_segments-vod-links-perms.sql similarity index 100% rename from services/migrations/migrations/00048_segments-vod-links-perms.sql rename to services/migrations-schema/migrations/00048_segments-vod-links-perms.sql diff --git a/services/migrations/migrations/00049_add-discord_message_id-on-vods.sql b/services/migrations-schema/migrations/00049_add-discord_message_id-on-vods.sql similarity index 100% rename from services/migrations/migrations/00049_add-discord_message_id-on-vods.sql rename to services/migrations-schema/migrations/00049_add-discord_message_id-on-vods.sql diff --git a/services/migrations/migrations/00050_add-status-to-vod.sql b/services/migrations-schema/migrations/00050_add-status-to-vod.sql similarity index 100% rename from services/migrations/migrations/00050_add-status-to-vod.sql rename to services/migrations-schema/migrations/00050_add-status-to-vod.sql diff --git a/services/migrations/migrations/00051_set-vod-status.sql b/services/migrations-schema/migrations/00051_set-vod-status.sql similarity index 100% rename from services/migrations/migrations/00051_set-vod-status.sql rename to services/migrations-schema/migrations/00051_set-vod-status.sql diff --git a/services/migrations/migrations/00052_correct-typo-filesize-to-bytes.sql b/services/migrations-schema/migrations/00052_correct-typo-filesize-to-bytes.sql similarity index 100% rename from services/migrations/migrations/00052_correct-typo-filesize-to-bytes.sql rename to services/migrations-schema/migrations/00052_correct-typo-filesize-to-bytes.sql diff --git a/services/migrations/migrations/00053_add-default-status-on-vods.sql b/services/migrations-schema/migrations/00053_add-default-status-on-vods.sql similarity index 100% rename from services/migrations/migrations/00053_add-default-status-on-vods.sql rename to services/migrations-schema/migrations/00053_add-default-status-on-vods.sql diff --git a/services/migrations/migrations/00054_remove-bytes-from-vod-tg-function.sql b/services/migrations-schema/migrations/00054_remove-bytes-from-vod-tg-function.sql similarity index 100% rename from services/migrations/migrations/00054_remove-bytes-from-vod-tg-function.sql rename to services/migrations-schema/migrations/00054_remove-bytes-from-vod-tg-function.sql diff --git a/services/migrations/migrations/00055_trigger_update_vod-switch-to-before.sql b/services/migrations-schema/migrations/00055_trigger_update_vod-switch-to-before.sql similarity index 100% rename from services/migrations/migrations/00055_trigger_update_vod-switch-to-before.sql rename to services/migrations-schema/migrations/00055_trigger_update_vod-switch-to-before.sql diff --git a/services/migrations/migrations/00056_add_job-retry-only-6-times.sql b/services/migrations-schema/migrations/00056_add_job-retry-only-6-times.sql similarity index 100% rename from services/migrations/migrations/00056_add_job-retry-only-6-times.sql rename to services/migrations-schema/migrations/00056_add_job-retry-only-6-times.sql diff --git a/services/migrations/migrations/00057_recreate-vod_create.sql b/services/migrations-schema/migrations/00057_recreate-vod_create.sql similarity index 100% rename from services/migrations/migrations/00057_recreate-vod_create.sql rename to services/migrations-schema/migrations/00057_recreate-vod_create.sql diff --git a/services/migrations/migrations/00058_conditionally-change-vod-status.sql b/services/migrations-schema/migrations/00058_conditionally-change-vod-status.sql similarity index 100% rename from services/migrations/migrations/00058_conditionally-change-vod-status.sql rename to services/migrations-schema/migrations/00058_conditionally-change-vod-status.sql diff --git a/services/migrations/migrations/00059_fix-filesize-typo.sql b/services/migrations-schema/migrations/00059_fix-filesize-typo.sql similarity index 100% rename from services/migrations/migrations/00059_fix-filesize-typo.sql rename to services/migrations-schema/migrations/00059_fix-filesize-typo.sql diff --git a/services/migrations/migrations/00060_create-recordings-table.sql b/services/migrations-schema/migrations/00060_create-recordings-table.sql similarity index 100% rename from services/migrations/migrations/00060_create-recordings-table.sql rename to services/migrations-schema/migrations/00060_create-recordings-table.sql diff --git a/services/migrations/migrations/00061_defaults-for-created_at-and-updated_at.sql b/services/migrations-schema/migrations/00061_defaults-for-created_at-and-updated_at.sql similarity index 100% rename from services/migrations/migrations/00061_defaults-for-created_at-and-updated_at.sql rename to services/migrations-schema/migrations/00061_defaults-for-created_at-and-updated_at.sql diff --git a/services/migrations/migrations/00062_create-discord_interactions.sql b/services/migrations-schema/migrations/00062_create-discord_interactions.sql similarity index 100% rename from services/migrations/migrations/00062_create-discord_interactions.sql rename to services/migrations-schema/migrations/00062_create-discord_interactions.sql diff --git a/services/migrations/migrations/00063_relate-discord_interactions-with-recordings.sql b/services/migrations-schema/migrations/00063_relate-discord_interactions-with-recordings.sql similarity index 100% rename from services/migrations/migrations/00063_relate-discord_interactions-with-recordings.sql rename to services/migrations-schema/migrations/00063_relate-discord_interactions-with-recordings.sql diff --git a/services/migrations/migrations/00064_execute-procedure-instead-of-function.sql b/services/migrations-schema/migrations/00064_execute-procedure-instead-of-function.sql similarity index 100% rename from services/migrations/migrations/00064_execute-procedure-instead-of-function.sql rename to services/migrations-schema/migrations/00064_execute-procedure-instead-of-function.sql diff --git a/services/migrations/migrations/00065_move-is_aborted-to-recordings.sql b/services/migrations-schema/migrations/00065_move-is_aborted-to-recordings.sql similarity index 100% rename from services/migrations/migrations/00065_move-is_aborted-to-recordings.sql rename to services/migrations-schema/migrations/00065_move-is_aborted-to-recordings.sql diff --git a/services/migrations/migrations/00066_add-segments-fk-to-vods.sql b/services/migrations-schema/migrations/00066_add-segments-fk-to-vods.sql similarity index 100% rename from services/migrations/migrations/00066_add-segments-fk-to-vods.sql rename to services/migrations-schema/migrations/00066_add-segments-fk-to-vods.sql diff --git a/services/migrations/migrations/00067_remove-duplicate-vods-segments.sql b/services/migrations-schema/migrations/00067_remove-duplicate-vods-segments.sql similarity index 100% rename from services/migrations/migrations/00067_remove-duplicate-vods-segments.sql rename to services/migrations-schema/migrations/00067_remove-duplicate-vods-segments.sql diff --git a/services/migrations/migrations/00068_remove-vod_id-from-recordings.sql b/services/migrations-schema/migrations/00068_remove-vod_id-from-recordings.sql similarity index 100% rename from services/migrations/migrations/00068_remove-vod_id-from-recordings.sql rename to services/migrations-schema/migrations/00068_remove-vod_id-from-recordings.sql diff --git a/services/migrations/migrations/00069_recreate-recordings-vod-relationship.sql b/services/migrations-schema/migrations/00069_recreate-recordings-vod-relationship.sql similarity index 100% rename from services/migrations/migrations/00069_recreate-recordings-vod-relationship.sql rename to services/migrations-schema/migrations/00069_recreate-recordings-vod-relationship.sql diff --git a/services/migrations/migrations/00070_relate-vod-to-recording.sql b/services/migrations-schema/migrations/00070_relate-vod-to-recording.sql similarity index 100% rename from services/migrations/migrations/00070_relate-vod-to-recording.sql rename to services/migrations-schema/migrations/00070_relate-vod-to-recording.sql diff --git a/services/migrations/migrations/00071_remove-recordings_vod_id_fkey.sql b/services/migrations-schema/migrations/00071_remove-recordings_vod_id_fkey.sql similarity index 100% rename from services/migrations/migrations/00071_remove-recordings_vod_id_fkey.sql rename to services/migrations-schema/migrations/00071_remove-recordings_vod_id_fkey.sql diff --git a/services/migrations/migrations/00072_create-builds-table.sql b/services/migrations-schema/migrations/00072_create-builds-table.sql similarity index 100% rename from services/migrations/migrations/00072_create-builds-table.sql rename to services/migrations-schema/migrations/00072_create-builds-table.sql diff --git a/services/migrations/migrations/00073_update-builds-table-timestamps.sql b/services/migrations-schema/migrations/00073_update-builds-table-timestamps.sql similarity index 100% rename from services/migrations/migrations/00073_update-builds-table-timestamps.sql rename to services/migrations-schema/migrations/00073_update-builds-table-timestamps.sql diff --git a/services/migrations/migrations/00074_switch-after-update-to-after-insert.sql b/services/migrations-schema/migrations/00074_switch-after-update-to-after-insert.sql similarity index 100% rename from services/migrations/migrations/00074_switch-after-update-to-after-insert.sql rename to services/migrations-schema/migrations/00074_switch-after-update-to-after-insert.sql diff --git a/services/migrations/migrations/00075_add-checksum-to-segments.sql b/services/migrations-schema/migrations/00075_add-checksum-to-segments.sql similarity index 100% rename from services/migrations/migrations/00075_add-checksum-to-segments.sql rename to services/migrations-schema/migrations/00075_add-checksum-to-segments.sql diff --git a/services/migrations/migrations/00076_builds-use-vod_id-instead-of-vod.sql b/services/migrations-schema/migrations/00076_builds-use-vod_id-instead-of-vod.sql similarity index 100% rename from services/migrations/migrations/00076_builds-use-vod_id-instead-of-vod.sql rename to services/migrations-schema/migrations/00076_builds-use-vod_id-instead-of-vod.sql diff --git a/services/migrations/migrations/00077_builds-trigger-change-vod-to-vod_id.sql b/services/migrations-schema/migrations/00077_builds-trigger-change-vod-to-vod_id.sql similarity index 100% rename from services/migrations/migrations/00077_builds-trigger-change-vod-to-vod_id.sql rename to services/migrations-schema/migrations/00077_builds-trigger-change-vod-to-vod_id.sql diff --git a/services/migrations/migrations/00078_add-bytes_uploaded-to-segments.sql b/services/migrations-schema/migrations/00078_add-bytes_uploaded-to-segments.sql similarity index 100% rename from services/migrations/migrations/00078_add-bytes_uploaded-to-segments.sql rename to services/migrations-schema/migrations/00078_add-bytes_uploaded-to-segments.sql diff --git a/services/migrations/migrations/00079_stream_id-optional.sql b/services/migrations-schema/migrations/00079_stream_id-optional.sql similarity index 100% rename from services/migrations/migrations/00079_stream_id-optional.sql rename to services/migrations-schema/migrations/00079_stream_id-optional.sql diff --git a/services/migrations/migrations/00080_add-created_at-to-vtubers.sql b/services/migrations-schema/migrations/00080_add-created_at-to-vtubers.sql similarity index 100% rename from services/migrations/migrations/00080_add-created_at-to-vtubers.sql rename to services/migrations-schema/migrations/00080_add-created_at-to-vtubers.sql diff --git a/services/migrations/migrations/00081_add-id_deprecated-to-vods.sql b/services/migrations-schema/migrations/00081_add-id_deprecated-to-vods.sql similarity index 100% rename from services/migrations/migrations/00081_add-id_deprecated-to-vods.sql rename to services/migrations-schema/migrations/00081_add-id_deprecated-to-vods.sql diff --git a/services/migrations/migrations/00082_add-id_deprecated-to-s3_files.sql b/services/migrations-schema/migrations/00082_add-id_deprecated-to-s3_files.sql similarity index 100% rename from services/migrations/migrations/00082_add-id_deprecated-to-s3_files.sql rename to services/migrations-schema/migrations/00082_add-id_deprecated-to-s3_files.sql diff --git a/services/migrations/migrations/00083_create_vods_s3_join.sql b/services/migrations-schema/migrations/00083_create_vods_s3_join.sql similarity index 100% rename from services/migrations/migrations/00083_create_vods_s3_join.sql rename to services/migrations-schema/migrations/00083_create_vods_s3_join.sql diff --git a/services/migrations/migrations/00084_add-file_id-to-s3_files.sql b/services/migrations-schema/migrations/00084_add-file_id-to-s3_files.sql similarity index 100% rename from services/migrations/migrations/00084_add-file_id-to-s3_files.sql rename to services/migrations-schema/migrations/00084_add-file_id-to-s3_files.sql diff --git a/services/migrations/migrations/00085_sync-s3_files.sql b/services/migrations-schema/migrations/00085_sync-s3_files.sql similarity index 100% rename from services/migrations/migrations/00085_sync-s3_files.sql rename to services/migrations-schema/migrations/00085_sync-s3_files.sql diff --git a/services/migrations/migrations/00086_add-cdn_url-to-s3_files.sql b/services/migrations-schema/migrations/00086_add-cdn_url-to-s3_files.sql similarity index 100% rename from services/migrations/migrations/00086_add-cdn_url-to-s3_files.sql rename to services/migrations-schema/migrations/00086_add-cdn_url-to-s3_files.sql diff --git a/services/migrations/migrations/00087_remove-id_deprecated.sql b/services/migrations-schema/migrations/00087_remove-id_deprecated.sql similarity index 100% rename from services/migrations/migrations/00087_remove-id_deprecated.sql rename to services/migrations-schema/migrations/00087_remove-id_deprecated.sql diff --git a/services/migrations/migrations/00088_remove-id_deprecated-from-vtubers.sql b/services/migrations-schema/migrations/00088_remove-id_deprecated-from-vtubers.sql similarity index 100% rename from services/migrations/migrations/00088_remove-id_deprecated-from-vtubers.sql rename to services/migrations-schema/migrations/00088_remove-id_deprecated-from-vtubers.sql diff --git a/services/migrations/migrations/00089_create-vods_s3_files_join.sql b/services/migrations-schema/migrations/00089_create-vods_s3_files_join.sql similarity index 100% rename from services/migrations/migrations/00089_create-vods_s3_files_join.sql rename to services/migrations-schema/migrations/00089_create-vods_s3_files_join.sql diff --git a/services/migrations/migrations/00090_rename-vods-s3-files-joins.sql b/services/migrations-schema/migrations/00090_rename-vods-s3-files-joins.sql similarity index 100% rename from services/migrations/migrations/00090_rename-vods-s3-files-joins.sql rename to services/migrations-schema/migrations/00090_rename-vods-s3-files-joins.sql diff --git a/services/migrations/migrations/00091_drop-vods_s3_file_join.sql b/services/migrations-schema/migrations/00091_drop-vods_s3_file_join.sql similarity index 100% rename from services/migrations/migrations/00091_drop-vods_s3_file_join.sql rename to services/migrations-schema/migrations/00091_drop-vods_s3_file_join.sql diff --git a/services/migrations/migrations/00092_use-s3_file_id.sql b/services/migrations-schema/migrations/00092_use-s3_file_id.sql similarity index 100% rename from services/migrations/migrations/00092_use-s3_file_id.sql rename to services/migrations-schema/migrations/00092_use-s3_file_id.sql diff --git a/services/migrations/migrations/00093_enable-pg_trgm.sql b/services/migrations-schema/migrations/00093_enable-pg_trgm.sql similarity index 100% rename from services/migrations/migrations/00093_enable-pg_trgm.sql rename to services/migrations-schema/migrations/00093_enable-pg_trgm.sql diff --git a/services/migrations/migrations/00094_add-patrons-table.sql b/services/migrations-schema/migrations/00094_add-patrons-table.sql similarity index 100% rename from services/migrations/migrations/00094_add-patrons-table.sql rename to services/migrations-schema/migrations/00094_add-patrons-table.sql diff --git a/services/migrations/migrations/00095_create-contributors.sql b/services/migrations-schema/migrations/00095_create-contributors.sql similarity index 100% rename from services/migrations/migrations/00095_create-contributors.sql rename to services/migrations-schema/migrations/00095_create-contributors.sql diff --git a/services/migrations/migrations/00096_add-id_num-to-s3_files.sql b/services/migrations-schema/migrations/00096_add-id_num-to-s3_files.sql similarity index 100% rename from services/migrations/migrations/00096_add-id_num-to-s3_files.sql rename to services/migrations-schema/migrations/00096_add-id_num-to-s3_files.sql diff --git a/services/migrations/migrations/00097_add-id_num-to-mux-assets.sql b/services/migrations-schema/migrations/00097_add-id_num-to-mux-assets.sql similarity index 100% rename from services/migrations/migrations/00097_add-id_num-to-mux-assets.sql rename to services/migrations-schema/migrations/00097_add-id_num-to-mux-assets.sql diff --git a/services/migrations/migrations/00098_add-id_num-to-streams.sql b/services/migrations-schema/migrations/00098_add-id_num-to-streams.sql similarity index 100% rename from services/migrations/migrations/00098_add-id_num-to-streams.sql rename to services/migrations-schema/migrations/00098_add-id_num-to-streams.sql diff --git a/services/migrations/migrations/00099_add-vtuber_num.sql b/services/migrations-schema/migrations/00099_add-vtuber_num.sql similarity index 100% rename from services/migrations/migrations/00099_add-vtuber_num.sql rename to services/migrations-schema/migrations/00099_add-vtuber_num.sql diff --git a/services/migrations/migrations/00100_add-id_num-to-tags.sql b/services/migrations-schema/migrations/00100_add-id_num-to-tags.sql similarity index 100% rename from services/migrations/migrations/00100_add-id_num-to-tags.sql rename to services/migrations-schema/migrations/00100_add-id_num-to-tags.sql diff --git a/services/migrations/migrations/00101_add-missing-cols-to-tags.sql b/services/migrations-schema/migrations/00101_add-missing-cols-to-tags.sql similarity index 100% rename from services/migrations/migrations/00101_add-missing-cols-to-tags.sql rename to services/migrations-schema/migrations/00101_add-missing-cols-to-tags.sql diff --git a/services/migrations/migrations/00102_add-toy_id_num-to-tags.sql b/services/migrations-schema/migrations/00102_add-toy_id_num-to-tags.sql similarity index 100% rename from services/migrations/migrations/00102_add-toy_id_num-to-tags.sql rename to services/migrations-schema/migrations/00102_add-toy_id_num-to-tags.sql diff --git a/services/migrations/migrations/00103_add-toy_num.sql b/services/migrations-schema/migrations/00103_add-toy_num.sql similarity index 100% rename from services/migrations/migrations/00103_add-toy_num.sql rename to services/migrations-schema/migrations/00103_add-toy_num.sql diff --git a/services/migrations/migrations/00104_add-toy_num-fix.sql b/services/migrations-schema/migrations/00104_add-toy_num-fix.sql similarity index 100% rename from services/migrations/migrations/00104_add-toy_num-fix.sql rename to services/migrations-schema/migrations/00104_add-toy_num-fix.sql diff --git a/services/migrations/migrations/00105_add-id_num-to-vods_links.sql b/services/migrations-schema/migrations/00105_add-id_num-to-vods_links.sql similarity index 100% rename from services/migrations/migrations/00105_add-id_num-to-vods_links.sql rename to services/migrations-schema/migrations/00105_add-id_num-to-vods_links.sql diff --git a/services/migrations/migrations/00106_add-id_num-to-vods.sql b/services/migrations-schema/migrations/00106_add-id_num-to-vods.sql similarity index 100% rename from services/migrations/migrations/00106_add-id_num-to-vods.sql rename to services/migrations-schema/migrations/00106_add-id_num-to-vods.sql diff --git a/services/migrations/migrations/00107_remove-uuid.sql b/services/migrations-schema/migrations/00107_remove-uuid.sql similarity index 100% rename from services/migrations/migrations/00107_remove-uuid.sql rename to services/migrations-schema/migrations/00107_remove-uuid.sql diff --git a/services/migrations/migrations/00108_add-strapi-tables.sql b/services/migrations-schema/migrations/00108_add-strapi-tables.sql similarity index 100% rename from services/migrations/migrations/00108_add-strapi-tables.sql rename to services/migrations-schema/migrations/00108_add-strapi-tables.sql diff --git a/services/migrations/migrations/00109_use-consistent-description_1.sql b/services/migrations-schema/migrations/00109_use-consistent-description_1.sql similarity index 100% rename from services/migrations/migrations/00109_use-consistent-description_1.sql rename to services/migrations-schema/migrations/00109_use-consistent-description_1.sql diff --git a/services/migrations-schema/migrations/00110_unseed-vtubers.sql b/services/migrations-schema/migrations/00110_unseed-vtubers.sql new file mode 100644 index 0000000..6af82b6 --- /dev/null +++ b/services/migrations-schema/migrations/00110_unseed-vtubers.sql @@ -0,0 +1,5 @@ +-- we are removing vtuber rows because vtubers are data. +-- data is not supposed to be part of schema migrations +-- @see https://github.com/zakpatterson/postgres-schema-migrations?tab=readme-ov-file#schema-migrations-vs-data-migrations + +DELETE FROM api.vtubers; \ No newline at end of file diff --git a/services/migrations/migrations/00111_create-strapi-tables-tvr.sql b/services/migrations-schema/migrations/00111_create-strapi-tables-tvr.sql similarity index 100% rename from services/migrations/migrations/00111_create-strapi-tables-tvr.sql rename to services/migrations-schema/migrations/00111_create-strapi-tables-tvr.sql diff --git a/services/migrations/migrations/00112_create-tvr_tag_links.sql b/services/migrations-schema/migrations/00112_create-tvr_tag_links.sql similarity index 100% rename from services/migrations/migrations/00112_create-tvr_tag_links.sql rename to services/migrations-schema/migrations/00112_create-tvr_tag_links.sql diff --git a/services/migrations/migrations/00113_create-tvr_vod_links.sql b/services/migrations-schema/migrations/00113_create-tvr_vod_links.sql similarity index 100% rename from services/migrations/migrations/00113_create-tvr_vod_links.sql rename to services/migrations-schema/migrations/00113_create-tvr_vod_links.sql diff --git a/services/migrations/migrations/00114_create-tags_toy_links.sql b/services/migrations-schema/migrations/00114_create-tags_toy_links.sql similarity index 100% rename from services/migrations/migrations/00114_create-tags_toy_links.sql rename to services/migrations-schema/migrations/00114_create-tags_toy_links.sql diff --git a/services/migrations/migrations/00115_create-tags_vods_links.sql b/services/migrations-schema/migrations/00115_create-tags_vods_links.sql similarity index 100% rename from services/migrations/migrations/00115_create-tags_vods_links.sql rename to services/migrations-schema/migrations/00115_create-tags_vods_links.sql diff --git a/services/migrations/migrations/00116_create-timestamps.sql b/services/migrations-schema/migrations/00116_create-timestamps.sql similarity index 100% rename from services/migrations/migrations/00116_create-timestamps.sql rename to services/migrations-schema/migrations/00116_create-timestamps.sql diff --git a/services/migrations/migrations/00117_create-timestamps_tag_links.sql b/services/migrations-schema/migrations/00117_create-timestamps_tag_links.sql similarity index 100% rename from services/migrations/migrations/00117_create-timestamps_tag_links.sql rename to services/migrations-schema/migrations/00117_create-timestamps_tag_links.sql diff --git a/services/migrations/migrations/00118_create-timestamps_vod_links.sql b/services/migrations-schema/migrations/00118_create-timestamps_vod_links.sql similarity index 100% rename from services/migrations/migrations/00118_create-timestamps_vod_links.sql rename to services/migrations-schema/migrations/00118_create-timestamps_vod_links.sql diff --git a/services/migrations/migrations/00119_create-toys_link_tag_links.sql b/services/migrations-schema/migrations/00119_create-toys_link_tag_links.sql similarity index 100% rename from services/migrations/migrations/00119_create-toys_link_tag_links.sql rename to services/migrations-schema/migrations/00119_create-toys_link_tag_links.sql diff --git a/services/migrations/migrations/00120_create-vods_mux_asset_links.sql b/services/migrations-schema/migrations/00120_create-vods_mux_asset_links.sql similarity index 100% rename from services/migrations/migrations/00120_create-vods_mux_asset_links.sql rename to services/migrations-schema/migrations/00120_create-vods_mux_asset_links.sql diff --git a/services/migrations/migrations/00121_create-vods_stream_links.sql b/services/migrations-schema/migrations/00121_create-vods_stream_links.sql similarity index 100% rename from services/migrations/migrations/00121_create-vods_stream_links.sql rename to services/migrations-schema/migrations/00121_create-vods_stream_links.sql diff --git a/services/migrations/migrations/00122_create-vods_thumbnail_links.sql b/services/migrations-schema/migrations/00122_create-vods_thumbnail_links.sql similarity index 100% rename from services/migrations/migrations/00122_create-vods_thumbnail_links.sql rename to services/migrations-schema/migrations/00122_create-vods_thumbnail_links.sql diff --git a/services/migrations/migrations/00123_create-vods_uploader_links.sql b/services/migrations-schema/migrations/00123_create-vods_uploader_links.sql similarity index 100% rename from services/migrations/migrations/00123_create-vods_uploader_links.sql rename to services/migrations-schema/migrations/00123_create-vods_uploader_links.sql diff --git a/services/migrations/migrations/00124_create-vods_video_src_b_2_links.sql b/services/migrations-schema/migrations/00124_create-vods_video_src_b_2_links.sql similarity index 100% rename from services/migrations/migrations/00124_create-vods_video_src_b_2_links.sql rename to services/migrations-schema/migrations/00124_create-vods_video_src_b_2_links.sql diff --git a/services/migrations/migrations/00125_create-vods_vtuber_links.sql b/services/migrations-schema/migrations/00125_create-vods_vtuber_links.sql similarity index 100% rename from services/migrations/migrations/00125_create-vods_vtuber_links.sql rename to services/migrations-schema/migrations/00125_create-vods_vtuber_links.sql diff --git a/services/migrations/migrations/00126_create-vtubers_toy_links.sql b/services/migrations-schema/migrations/00126_create-vtubers_toy_links.sql similarity index 100% rename from services/migrations/migrations/00126_create-vtubers_toy_links.sql rename to services/migrations-schema/migrations/00126_create-vtubers_toy_links.sql diff --git a/services/migrations/migrations/00127_grant-permissions-for-vods_vtuber_links.sql b/services/migrations-schema/migrations/00127_grant-permissions-for-vods_vtuber_links.sql similarity index 100% rename from services/migrations/migrations/00127_grant-permissions-for-vods_vtuber_links.sql rename to services/migrations-schema/migrations/00127_grant-permissions-for-vods_vtuber_links.sql diff --git a/services/migrations/migrations/00128_add-vtuber-fk-to-vods.sql b/services/migrations-schema/migrations/00128_add-vtuber-fk-to-vods.sql similarity index 100% rename from services/migrations/migrations/00128_add-vtuber-fk-to-vods.sql rename to services/migrations-schema/migrations/00128_add-vtuber-fk-to-vods.sql diff --git a/services/migrations/migrations/00129_add-vtuber_id-to-vods.sql b/services/migrations-schema/migrations/00129_add-vtuber_id-to-vods.sql similarity index 100% rename from services/migrations/migrations/00129_add-vtuber_id-to-vods.sql rename to services/migrations-schema/migrations/00129_add-vtuber_id-to-vods.sql diff --git a/services/migrations/migrations/00130_add-s3_files-fk-to-vods.sql b/services/migrations-schema/migrations/00130_add-s3_files-fk-to-vods.sql similarity index 100% rename from services/migrations/migrations/00130_add-s3_files-fk-to-vods.sql rename to services/migrations-schema/migrations/00130_add-s3_files-fk-to-vods.sql diff --git a/services/migrations/migrations/00131_grant-perms-on-s3_files.sql b/services/migrations-schema/migrations/00131_grant-perms-on-s3_files.sql similarity index 100% rename from services/migrations/migrations/00131_grant-perms-on-s3_files.sql rename to services/migrations-schema/migrations/00131_grant-perms-on-s3_files.sql diff --git a/services/migrations/migrations/00132_rename-vtuber_num-to-vtuber_id.sql b/services/migrations-schema/migrations/00132_rename-vtuber_num-to-vtuber_id.sql similarity index 100% rename from services/migrations/migrations/00132_rename-vtuber_num-to-vtuber_id.sql rename to services/migrations-schema/migrations/00132_rename-vtuber_num-to-vtuber_id.sql diff --git a/services/migrations/migrations/00133_add-mux_assets-fk-to-vods.sql b/services/migrations-schema/migrations/00133_add-mux_assets-fk-to-vods.sql similarity index 100% rename from services/migrations/migrations/00133_add-mux_assets-fk-to-vods.sql rename to services/migrations-schema/migrations/00133_add-mux_assets-fk-to-vods.sql diff --git a/services/migrations/migrations/00134_add-thumbnail-fk-to-vods.sql b/services/migrations-schema/migrations/00134_add-thumbnail-fk-to-vods.sql similarity index 100% rename from services/migrations/migrations/00134_add-thumbnail-fk-to-vods.sql rename to services/migrations-schema/migrations/00134_add-thumbnail-fk-to-vods.sql diff --git a/services/migrations-schema/migrations/00135_add-b2_files-permissions.sql b/services/migrations-schema/migrations/00135_add-b2_files-permissions.sql new file mode 100644 index 0000000..b30808a --- /dev/null +++ b/services/migrations-schema/migrations/00135_add-b2_files-permissions.sql @@ -0,0 +1,2 @@ +GRANT all ON api.b2_files TO automation; +GRANT SELECT ON api.b2_files TO web_anon; diff --git a/services/migrations-schema/migrations/00136_add-uuid-to-streams.sql b/services/migrations-schema/migrations/00136_add-uuid-to-streams.sql new file mode 100644 index 0000000..69151dc --- /dev/null +++ b/services/migrations-schema/migrations/00136_add-uuid-to-streams.sql @@ -0,0 +1,8 @@ +ALTER TABLE IF EXISTS api.streams + ADD COLUMN IF NOT EXISTS uuid UUID + DEFAULT gen_random_uuid(); + +ALTER TABLE IF EXISTS api.vods + ADD COLUMN IF NOT EXISTS uuid UUID + DEFAULT gen_random_uuid(); + diff --git a/services/migrations-schema/migrations/00137_add-vods-fk-to-streams.sql b/services/migrations-schema/migrations/00137_add-vods-fk-to-streams.sql new file mode 100644 index 0000000..c38ceaa --- /dev/null +++ b/services/migrations-schema/migrations/00137_add-vods-fk-to-streams.sql @@ -0,0 +1,4 @@ +ALTER TABLE api.vods + ADD CONSTRAINT vods_stream_fk FOREIGN KEY (stream_id) + REFERENCES api.streams (id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/services/migrations-schema/migrations/00138_add-date_2-to-streams-and-vods.sql b/services/migrations-schema/migrations/00138_add-date_2-to-streams-and-vods.sql new file mode 100644 index 0000000..e234f37 --- /dev/null +++ b/services/migrations-schema/migrations/00138_add-date_2-to-streams-and-vods.sql @@ -0,0 +1,7 @@ + + +ALTER TABLE IF EXISTS api.streams + ADD COLUMN IF NOT EXISTS date_2 TIMESTAMP(6) WITH TIME ZONE; + +ALTER TABLE IF EXISTS api.vods + ADD COLUMN IF NOT EXISTS date_2 TIMESTAMP(6) WITH TIME ZONE; \ No newline at end of file diff --git a/services/migrations/package.json b/services/migrations-schema/package.json similarity index 80% rename from services/migrations/package.json rename to services/migrations-schema/package.json index 38c2b95..2019d04 100644 --- a/services/migrations/package.json +++ b/services/migrations-schema/package.json @@ -1,5 +1,5 @@ { - "name": "@futureporn/migrations", + "name": "@futureporn/migrations-schema", "type": "module", "version": "0.6.0", "description": "", @@ -14,6 +14,6 @@ "license": "Unlicense", "dependencies": { "dotenv": "^16.4.5", - "postgres-migrations": "^5.3.0" + "postgres-schema-migrations": "^6.1.0" } } diff --git a/services/migrations/pnpm-lock.yaml b/services/migrations-schema/pnpm-lock.yaml similarity index 87% rename from services/migrations/pnpm-lock.yaml rename to services/migrations-schema/pnpm-lock.yaml index dfb50db..535b76b 100644 --- a/services/migrations/pnpm-lock.yaml +++ b/services/migrations-schema/pnpm-lock.yaml @@ -11,9 +11,41 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 - postgres-migrations: - specifier: ^5.3.0 - version: 5.3.0 + postgres-schema-migrations: + specifier: ^6.1.0 + version: 6.1.0 + + ../..: {} + + ../../packages/fetchers: {} + + ../../packages/infra: {} + + ../../packages/storage: {} + + ../../packages/types: {} + + ../../packages/utils: {} + + ../bot: {} + + ../capture: {} + + ../factory: {} + + ../htmx: {} + + ../mailbox: {} + + ../migrations-data: {} + + ../next: {} + + ../scout: {} + + ../strapi: {} + + ../uppy: {} packages: @@ -71,8 +103,8 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres-migrations@5.3.0: - resolution: {integrity: sha512-gnTHWJZVWbW8T3mXIxJm1JRU853TqBVWkhgfsTJr7zqT3VexjRmJj9kNi96rVhfTezDU4FVW6pf701kLOZiKIA==} + postgres-schema-migrations@6.1.0: + resolution: {integrity: sha512-d1LJ+A9Lg4kAwuh91S8ozF8q3adFNJlStbpUF/sbjMTzSIzJClpmg4D6qyd9nvKt2el0rnZJjXZQ2r01Y5OpzA==} engines: {node: '>10.17.0'} hasBin: true @@ -137,7 +169,7 @@ snapshots: dependencies: xtend: 4.0.2 - postgres-migrations@5.3.0: + postgres-schema-migrations@6.1.0: dependencies: pg: 8.12.0 sql-template-strings: 2.2.2 diff --git a/services/migrations/migrations/00110_unseed-vtubers.sql b/services/migrations/migrations/00110_unseed-vtubers.sql deleted file mode 100644 index 7b73e3e..0000000 --- a/services/migrations/migrations/00110_unseed-vtubers.sql +++ /dev/null @@ -1,5 +0,0 @@ --- we are removing vtuber rows because vtubers are data. --- data is not supposed to be part of schema migrations --- @see https://github.com/thomwright/postgres-migrations?tab=readme-ov-file#schema-migrations-vs-data-migrations - -DELETE FROM api.vtubers; \ No newline at end of file diff --git a/services/next/.gitignore b/services/next/.gitignore index 473707c..c43ed62 100644 --- a/services/next/.gitignore +++ b/services/next/.gitignore @@ -1,6 +1,7 @@ # Created by https://www.toptal.com/developers/gitignore/api/nextjs # Edit at https://www.toptal.com/developers/gitignore?templates=nextjs +fuck.mjs .vscode/ diff --git a/services/next/app/api/patreon/currently-entitled-tiers/route.ts b/services/next/app/api/patreon/currently-entitled-tiers/route.ts index 89fe62f..46f4a93 100644 --- a/services/next/app/api/patreon/currently-entitled-tiers/route.ts +++ b/services/next/app/api/patreon/currently-entitled-tiers/route.ts @@ -8,6 +8,8 @@ import { authOptions } from "@/app/lib/auth"; export async function GET(req: Request, res: Response) { + // return NextResponse.json(['test']) + console.log('lets run getServerSession()') @@ -19,6 +21,10 @@ export async function GET(req: Request, res: Response) { if (session) { + // console.log('currently-entitled-tiers lets get user id') + // console.log(session) + + let keycloakIdpToken, patreonTiersList if (!session.token?.access_token) { @@ -26,23 +32,30 @@ export async function GET(req: Request, res: Response) { return NextResponse.json({ error: `Failed to get access token from Session`}, { status: 500 }) } + if (!session.token.sub) { + return NextResponse.json({ error: `failed to get profile.sub from Session` }, { status: 400 }) + } + try { keycloakIdpToken = await getKeycloakIdpToken(session.token.access_token) } catch (e) { + console.error(e) return NextResponse.json({ error: `Failed to get Patreon token (Keycloak IDP). e=${e}`}, { status: 401 }) } try { patreonTiersList = await getPatreonMemberships(keycloakIdpToken) } catch (e) { + console.error(e) return NextResponse.json({ error: `Failed to get patreon memberships. e=${e}`}, { status: 401 }) } // side effect which grants the appropriate keycloak roles to the user try { - await syncronizeKeycloakRoles(session.user.id, patreonTiersList) + await syncronizeKeycloakRoles(session.token.sub, patreonTiersList) } catch (e) { + console.error(e) return NextResponse.json({ error: `Failed to syncronize roles` }, { status: 500 }) } diff --git a/services/next/app/api/profile/route.ts b/services/next/app/api/profile/route.ts new file mode 100644 index 0000000..5c58f29 --- /dev/null +++ b/services/next/app/api/profile/route.ts @@ -0,0 +1,51 @@ + + +// 'use server'; + +// import { NextRequest, NextResponse } from "next/server"; +// import { getServerSession } from "next-auth"; +// import { authOptions } from "@/app/lib/auth"; +// import { getUMAToken } from "@/app/lib/keycloak"; +// import { configs } from "@/app/config/configs"; + +// export async function POST(req: NextRequest) { +// const session = await getServerSession(authOptions); + +// console.log(`a user is attempting to POST their profile. session as follows`) +// console.log(session) + +// if (session) { +// if (!session.token?.access_token) { +// console.error('session.token.access_token was missing') +// return NextResponse.json({ error: `Failed to get access token from Session`}, { status: 500 }) +// } + +// const uma = await getUMAToken(); +// const userId = session.user.id; // Assuming session.user.id is the Keycloak user ID +// const updatedAttributes = await req.json(); // Assuming the updated attributes are sent in the request body + +// console.log('updatedAttributes as follows') +// console.log(updatedAttributes) + +// const response = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}`, { +// method: 'PUT', +// headers: { +// 'Authorization': `Bearer ${uma.access_token}`, +// 'Content-Type': 'application/json' +// }, +// body: JSON.stringify({ +// attributes: updatedAttributes +// }) +// }); + +// // /admin/realms/futureporn/users/9dd538cd-56a9-4f43-81cd-fa86473214f5 + +// if (!response.ok) { +// console.error(`failed. res.status=${response.status} res.statusText=${response.statusText}`) +// return NextResponse.json({ error: 'Failed to update user attributes' }, { status: 500 }); +// } else { +// return NextResponse.json({ message: 'User attributes updated successfully' }); +// } +// } +// return NextResponse.json({ error: "You must be logged in." }, { status: 401 }); +// } \ No newline at end of file diff --git a/services/next/app/archive/[cuid]/page.tsx b/services/next/app/archive/[cuid]/page.tsx deleted file mode 100644 index 37ae352..0000000 --- a/services/next/app/archive/[cuid]/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ - -import StreamPage from '@/app/components/stream-page'; -import { getStreamByCuid } from '@/app/lib/streams'; - - -interface IPageParams { - params: { - cuid: string; - } -} - - -export default async function Page ({ params: { cuid } }: IPageParams) { - const stream = await getStreamByCuid(cuid); - console.log(`getting stream by cuid. cuid=${cuid}`) - return ( - <> - - - ) -} \ No newline at end of file diff --git a/services/next/app/archive/[cuid]/not-found.tsx b/services/next/app/archive/[uuid]/not-found.tsx similarity index 100% rename from services/next/app/archive/[cuid]/not-found.tsx rename to services/next/app/archive/[uuid]/not-found.tsx diff --git a/services/next/app/archive/[uuid]/page.tsx b/services/next/app/archive/[uuid]/page.tsx new file mode 100644 index 0000000..b9f03a8 --- /dev/null +++ b/services/next/app/archive/[uuid]/page.tsx @@ -0,0 +1,21 @@ + +import StreamPage from '@/app/components/stream-page'; +import { getStreamByUUID } from '@/app/lib/streams'; + + +interface IPageParams { + params: { + uuid: string; + } +} + + +export default async function Page ({ params: { uuid } }: IPageParams) { + const stream = await getStreamByUUID(uuid); + console.log(`getting stream by uuid. uuid=${uuid}`) + return ( + <> + + + ) +} \ No newline at end of file diff --git a/services/next/app/archive/page.tsx b/services/next/app/archive/page.tsx index f9c17bd..c38ca75 100644 --- a/services/next/app/archive/page.tsx +++ b/services/next/app/archive/page.tsx @@ -8,68 +8,7 @@ import { notFound } from "next/navigation"; export default async function Page() { - // const vtubers = await getAllVtubers(); - // const streams = await getAllStreams(); - // const streams = await getStreamsForVtuber(1) - // const pageSize = 100; - // const page = 1; - - - // export interface IStream { - // id: number; - // attributes: { - // date: string; - // archiveStatus: 'good' | 'issue' | 'missing'; - // vods: IVodsResponse; - // cuid: string; - // vtuber: IVtuberResponse; - // tweet: ITweetResponse; - // isChaturbateStream: boolean; - // isFanslyStream: boolean; - // } - // } - - // if (!vtubers) notFound(); - // const streams = [ - // { - // "firstName": "Tanner", - // "lastName": "Linsley", - // "age": 33, - // "visits": 100, - // "progress": 50, - // "status": "Married", - // "id": 5, - // "attributes": { - // date: '2023-10-10T15:18:20.003Z', - // archiveStatus: 'missing', - // isChaturbateStream: false, - // isFanslyStream: true, - // vods: {}, - // cuid: '2983482932384', - // vtuber: {}, - // tweet: '', - // } - // }, - // { - // "firstName": "Kevin", - // "lastName": "Vandy", - // "age": 27, - // "visits": 200, - // "progress": 100, - // "status": "Single", - // "id": 3, - // "attributes": { - // date: '2023-10-10T15:18:20.003Z', - // archiveStatus: 'missing', - // isChaturbateStream: true, - // isFanslyStream: true, - // vods: {}, - // cuid: '29823432384', - // vtuber: {}, - // tweet: '', - // } - // } - // ] + return (
diff --git a/services/next/app/components/stream-button.tsx b/services/next/app/components/stream-button.tsx index 2e36648..97b946f 100644 --- a/services/next/app/components/stream-button.tsx +++ b/services/next/app/components/stream-button.tsx @@ -8,10 +8,10 @@ export function StreamButton({ stream }: { stream: IStream }) { return ( - {new Date(stream.attributes.date).toLocaleDateString()} + {new Date(stream.date).toLocaleDateString()} ) } \ No newline at end of file diff --git a/services/next/app/components/stream-page.tsx b/services/next/app/components/stream-page.tsx index 0d5aa95..08d5d53 100644 --- a/services/next/app/components/stream-page.tsx +++ b/services/next/app/components/stream-page.tsx @@ -1,7 +1,6 @@ 'use client'; import { IStream } from "@futureporn/types"; -// import NotFound from "app/streams/[cuid]/not-found"; import { IVod } from "@/app/lib/vods"; import Link from "next/link"; import Image from "next/legacy/image"; @@ -29,15 +28,15 @@ function capitalizeFirstLetter(string: string): string { } function hasNote(vod: IVod) { - if (!!vod?.attributes?.note) return true; + if (!!vod?.note) return true; else return false; } function determineStatus(stream: IStream): Status { - if (stream.attributes.vods.data.length < 1) { + if (stream.vods.length < 1) { return 'missing' } else { - if (stream.attributes.vods.data.some((vod: IVod) => !hasNote(vod))) { + if (stream.vods.some((vod: IVod) => !hasNote(vod))) { return 'good'; } else { return 'issue'; @@ -49,8 +48,8 @@ export default function StreamPage({ stream }: IStreamProps) { console.log('StreamPage function has been invoked! stream as follows') console.log(stream) if (!stream) notFound() - const displayName = stream.attributes.vtuber.data.attributes.displayName; - const date = new Date(stream.attributes.date); + const displayName = stream.vtuber.display_name; + const date = new Date(stream.date); const [hemisphere, setHemisphere] = useState(Hemisphere.NORTHERN); const [selectedStatus, setSelectedStatus] = useState(determineStatus(stream)); @@ -105,8 +104,8 @@ export default function StreamPage({ stream }: IStreamProps) { // ].filter(Boolean).join(', ') const platformsList = [ - (stream.attributes.isChaturbateStream && 'CB'), - (stream.attributes.isFanslyStream && 'Fansly') + (stream.is_chaturbate_stream && 'CB'), + (stream.is_fansly_stream && 'Fansly') ].filter(Boolean).join(', ') || '!!!'; @@ -171,7 +170,7 @@ export default function StreamPage({ stream }: IStreamProps) {

{desc1}

{desc2}
- Upload it here.

+ Upload it here.

@@ -179,7 +178,7 @@ export default function StreamPage({ stream }: IStreamProps) { - {stream.attributes.vods.data.length !== 0 && + {stream.vods.length !== 0 &&

VODs

@@ -195,16 +194,16 @@ export default function StreamPage({ stream }: IStreamProps) { - {stream.attributes.vods.data.map((vod: IVod) => ( + {stream.vods.map((vod: IVod) => ( {/*

{JSON.stringify(vod, null, 2)}

*/} - - + + {/* */} - - - + + + ))} diff --git a/services/next/app/components/streams-table.tsx b/services/next/app/components/streams-table.tsx index 316f6b3..fd7a2ee 100644 --- a/services/next/app/components/streams-table.tsx +++ b/services/next/app/components/streams-table.tsx @@ -48,79 +48,79 @@ export default function StreamsTable() { // archiveStatus const columns = React.useMemo[]>( () => [ - { - header: 'VTuber', - accessorFn: d => ({ - displayName: d.vtuber.display_name, - image: d.vtuber.image, - imageBlur: d.vtuber.image_blur - }), - cell: info => { - const { displayName, image, imageBlur } = info.getValue<{ displayName: string, image: string, imageBlur: string }>(); - return ( - <> -
-
-
- -
-
-
- {displayName} -
+ { + header: 'VTuber', + accessorFn: d => ({ + displayName: d.vtuber.display_name, + image: d.vtuber.image, + imageBlur: d.vtuber.image_blur + }), + cell: info => { + const { displayName, image, imageBlur } = info.getValue<{ displayName: string, image: string, imageBlur: string }>(); + return ( + <> +
+
+
+ +
- - ) - } - }, - { - header: 'Date', - accessorFn: d => format(new Date(d.date), 'yyyy-MM-dd HH:mm'), - sortingFn: 'datetime', - sortDescFirst: true, - cell: info => ({info.getValue() as string}) - }, - { - header: 'Platform', - accessorFn: d => [ - (d.is_chaturbate_stream && 'CB'), - (d.is_fansly_stream && 'Fansly') - ].filter(Boolean).join(', ') || '???' - }, - { - header: 'Status', - accessorFn: d => { - if (!d.archive_status) return 'missing'; - return d.archive_status - } - }, - // { - // header: 'Name', - // footer: props => props.column.id, - // columns: [ - // { - // accessorKey: 'firstName', - // cell: info => info.getValue(), - // footer: props => props.column.id, - // }, - // { - // accessorFn: row => row.lastName, - // id: 'lastName', - // cell: info => info.getValue(), - // header: () => Last Name, - // footer: props => props.column.id, - // }, - // ], - // }, +
+ {displayName} +
+
+ + ) + } + }, + { + header: 'Date', + accessorFn: d => format(new Date(d.date), 'yyyy-MM-dd HH:mm'), + sortingFn: 'datetime', + sortDescFirst: true, + cell: info => ({info.getValue() as string}) + }, + { + header: 'Platform', + accessorFn: d => [ + (d.is_chaturbate_stream && 'CB'), + (d.is_fansly_stream && 'Fansly') + ].filter(Boolean).join(', ') || '???' + }, + { + header: 'Status', + accessorFn: d => { + if (!d.archive_status) return 'missing'; + return d.archive_status + } + }, + // { + // header: 'Name', + // footer: props => props.column.id, + // columns: [ + // { + // accessorKey: 'firstName', + // cell: info => info.getValue(), + // footer: props => props.column.id, + // }, + // { + // accessorFn: row => row.lastName, + // id: 'lastName', + // cell: info => info.getValue(), + // header: () => Last Name, + // footer: props => props.column.id, + // }, + // ], + // }, ], [] ) @@ -157,8 +157,8 @@ export default function StreamsTable() { return (
- - {isPending && } + + {isPending && } {!isPending && <>
{vod.attributes.cuid}{vod.attributes.publishedAt}{vod.uuid}{vod.publishedAt}{(!!vod?.attributes?.thumbnail?.data?.attributes?.cdnUrl) ? : } {(!!vod?.attributes?.duration) ? vod.attributes.duration : }{vod.attributes.tagVodRelations.data.length}{vod.attributes.timestamps.data.length}{(!!vod.attributes.note) ? : }{vod.tagVodRelations.length}{vod.timestamps.length}{(!!vod.note) ? : }
@@ -187,8 +187,8 @@ export default function StreamsTable() { {row.getVisibleCells().map(cell => { return ( -
{flexRender( @@ -203,86 +203,85 @@ export default function StreamsTable() { })}
- - -
-
- - - - -
-
- Page - - {table.getState().pagination.pageIndex + 1} of{' '} - {table.getPageCount().toLocaleString()} - -
-
- {/* second row with page number input and pages-per-screen select */} -
-
- Go to page: -
-
- { - const page = e.target.value ? Number(e.target.value) - 1 : 0 - table.setPageIndex(page) - }} - className="input" - /> -
-
-
- + {'<<'} + + + + +
+
+ Page + + {table.getState().pagination.pageIndex + 1} of{' '}{table.getPageCount().toLocaleString()} +
-
- } + + {/* second row with page number input and pages-per-screen select */} +
+
+ Go to page: +
+
+ { + const page = e.target.value ? Number(e.target.value) - 1 : 0 + table.setPageIndex(page) + }} + className="input" + /> +
+
+
+ +
+
+
+ } -
- + + ) } diff --git a/services/next/app/components/vod-card.tsx b/services/next/app/components/vod-card.tsx index 1d8ba2b..ad2a2df 100644 --- a/services/next/app/components/vod-card.tsx +++ b/services/next/app/components/vod-card.tsx @@ -3,6 +3,7 @@ import { getSafeDate } from '@/app/lib/dates'; import { IVtuber } from '@futureporn/types'; import Image from "next/legacy/image" import { LocalizedDate } from '@/app/components/localized-date' +import Skeleton from "react-loading-skeleton"; interface IVodCardProps { id: number; @@ -24,19 +25,21 @@ export default function VodCard({id, title, date, muxAsset, thumbnail = 'https:/
- {/* @todo restore thumbnail image */} - {/*
- {title} -
*/} + {thumbnail && +
+ {title} +
+ } + {!thumbnail &&

missing thumbnail

}

{vtuber.display_name}

diff --git a/services/next/app/config/clientConfigs.ts b/services/next/app/config/clientConfigs.ts index d5723a2..2442989 100644 --- a/services/next/app/config/clientConfigs.ts +++ b/services/next/app/config/clientConfigs.ts @@ -1,6 +1,11 @@ if (!process.env.NEXT_PUBLIC_API_DOMAIN) throw new Error('Missing NEXT_PUBLIC_API_DOMAIN env var'); if (!process.env.NEXT_PUBLIC_WEBSITE_DOMAIN) throw new Error('Missing NEXT_PUBLIC_WEBSITE_DOMAIN env var'); +if (!process.env.NEXT_PUBLIC_UPPY_COMPANION_URL) throw new Error('Missing NEXT_PUBLIC_UPPY_COMPANION_URL env var'); +if (!process.env.NEXT_PUBLIC_SITE_URL) throw new Error('Missing NEXT_PUBLIC_SITE_URL env var'); +if (!process.env.NEXT_PUBLIC_STRAPI_URL) throw new Error('Missing NEXT_PUBLIC_STRAPI_URL env var'); +if (!process.env.NEXT_PUBLIC_POSTGREST_URL) throw new Error('Missing NEXT_PUBLIC_POSTGREST_URL env var'); + // const apiDomain = process.env.NEXT_PUBLIC_API_DOMAIN! // const websiteDomain = process.env.NEXT_PUBLIC_WEBSITE_DOMAIN! diff --git a/services/next/app/config/configs.ts b/services/next/app/config/configs.ts index 653d059..c0134ea 100644 --- a/services/next/app/config/configs.ts +++ b/services/next/app/config/configs.ts @@ -1,21 +1,16 @@ - -if (!process.env.PATREON_CLIENT_ID) throw new Error("PATREON_CLIENT_ID was missing from env"); -if (!process.env.PATREON_CLIENT_SECRET) throw new Error('Missing PATREON_CLIENT_SECRET env var'); -if (!process.env.KEYCLOAK_CLIENT_ID) throw new Error(`KEYCLOAK_CLIENT_ID missing in env!`); -if (!process.env.KEYCLOAK_CLIENT_SECRET) throw new Error(`KEYCLOAK_CLIENT_SECRET missing in env!`); -if (!process.env.KEYCLOAK_ISSUER) throw new Error(`KEYCLOAK_ISSUER missing in env!`); -if (!process.env.NEXTAUTH_SECRET) throw new Error(`NEXTAUTH_SECRET missing in env!`); -if (!process.env.NEXTAUTH_URL) throw new Error(`NEXTAUTH_URL missing in env!`); - +// When adding env vars here, make sure to also add the assertion in ./server.ts const patreonClientId = process.env.PATREON_CLIENT_ID! const patreonClientSecret = process.env.PATREON_CLIENT_SECRET! const keycloakClientId = process.env.KEYCLOAK_CLIENT_ID! const keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET! const keycloakIssuer = process.env.KEYCLOAK_ISSUER! +const keycloakLocalUrl = process.env.KEYCLOAK_LOCAL_URL! +const keycloakUrl = process.env.KEYCLOAK_URL! const nextAuthSecret = process.env.NEXTAUTH_SECRET! const nextAuthUrl = process.env.NEXTAUTH_URL! +const nextUrl = process.env.NEXT_PUBLIC_URL! export interface ServerConfig { patreonClientId: string; @@ -23,8 +18,11 @@ export interface ServerConfig { keycloakClientId: string; keycloakClientSecret: string; keycloakIssuer: string; + keycloakLocalUrl: string; + keycloakUrl: string; nextAuthSecret: string; nextAuthUrl: string; + nextUrl: string; } export const configs: ServerConfig = { @@ -33,6 +31,9 @@ export const configs: ServerConfig = { keycloakClientId, keycloakClientSecret, keycloakIssuer, + keycloakLocalUrl, + keycloakUrl, nextAuthSecret, nextAuthUrl, + nextUrl, } \ No newline at end of file diff --git a/services/next/app/config/server.ts b/services/next/app/config/server.ts new file mode 100644 index 0000000..a70e23c --- /dev/null +++ b/services/next/app/config/server.ts @@ -0,0 +1,20 @@ +/** + * this file exists to give us verbose errors when we forget to define a server-side env var. + * configs should go in ./configs + */ + +'use server'; + +export async function main () { + if (!process.env.PATREON_CLIENT_ID) throw new Error("PATREON_CLIENT_ID was missing from env"); + if (!process.env.PATREON_CLIENT_SECRET) throw new Error('Missing PATREON_CLIENT_SECRET env var'); + if (!process.env.KEYCLOAK_CLIENT_ID) throw new Error(`KEYCLOAK_CLIENT_ID missing in env!`); + if (!process.env.KEYCLOAK_CLIENT_SECRET) throw new Error(`KEYCLOAK_CLIENT_SECRET missing in env!`); + if (!process.env.KEYCLOAK_ISSUER) throw new Error(`KEYCLOAK_ISSUER missing in env!`); + if (!process.env.KEYCLOAK_LOCAL_URL) throw new Error(`KEYCLOAK_LOCAL_URL missing in env!`); + if (!process.env.NEXTAUTH_SECRET) throw new Error(`NEXTAUTH_SECRET missing in env!`); + if (!process.env.NEXTAUTH_URL) throw new Error(`NEXTAUTH_URL missing in env!`); + if (!process.env.NEXT_PUBLIC_URL) throw new Error(`NEXT_PUBLIC_URL missing in env!`); +} + +main() diff --git a/services/next/app/latest-vods/[page]/page.tsx b/services/next/app/latest-vods/[page]/page.tsx index 93ba533..17ebc96 100644 --- a/services/next/app/latest-vods/[page]/page.tsx +++ b/services/next/app/latest-vods/[page]/page.tsx @@ -9,23 +9,20 @@ interface IPageParams { } export default async function Page({ params: { page } }: IPageParams) { - let vods; - try { - vods = await getVods(page, 24, true); - } catch (error) { - console.error("An error occurred:", error); - return
Error: {JSON.stringify(error)}
; - } + + const pageSize = 24 + const { count, vods } = await getVods(page, pageSize, true); + return ( <>

Latest VODs

page {page}

- + ); diff --git a/services/next/app/lib/auth.ts b/services/next/app/lib/auth.ts index fff8f94..1c7a690 100644 --- a/services/next/app/lib/auth.ts +++ b/services/next/app/lib/auth.ts @@ -4,7 +4,8 @@ import { jwtDecode } from "jwt-decode"; import { type JWT } from "next-auth/jwt"; import { User, Profile } from "next-auth"; import { configs } from "../config/configs"; -import { getPatreonMemberships, getKeycloakIdpToken, updateKeycloakUserPatreonEntitlements } from "./patreon"; +import { getPatreonMemberships, getKeycloakIdpToken, updateKeycloakUserPatreonEntitlements, tiersToRolesMap } from "./patreon"; +import { getTierNameFromTierId } from "./keycloak"; declare module "next-auth" { @@ -41,6 +42,9 @@ declare module "next-auth/jwt" { export const authOptions: NextAuthOptions = { + session: { + strategy: "jwt" + }, providers: [ KeycloakProvider({ clientId: configs.keycloakClientId, @@ -49,51 +53,122 @@ export const authOptions: NextAuthOptions = { }) ], callbacks: { - async jwt({ token, account, profile }) { - - // console.log(`jwt() callback. account as follows.`) - // console.log(account) - // console.log(`jwt() callback. token as follows.`) - // console.log(token) - console.log(`jwt() callback. profile as follows.`) - console.log(profile) - - if (account) { + + async jwt({ token, account, profile }): Promise { + if (account) { + // First-time login, save the `access_token`, its expiry and the `refresh_token` const decodedAccountToken = jwtDecode(account.access_token as any) as KeycloakProfile - console.log(`jwt() callback. decodedAccountToken as follows.`) - console.log(decodedAccountToken) - token.client_roles = decodedAccountToken.realm_access.roles - token.scope = decodedAccountToken.scope + return { + ...token, + access_token: account.access_token, + expires_at: account.expires_at, + refresh_token: account.refresh_token, + client_roles: decodedAccountToken.realm_access.roles, + scope: decodedAccountToken.scope, + resource_access: decodedAccountToken.resource_access, + profile: profile, + } + } else if (Date.now() < token.expires_at * 1000) { + // Subsequent logins, but the `access_token` is still valid + return token + } else { + // Subsequent logins, but the `access_token` has expired, try to refresh it + if (!token.refresh_token) throw new TypeError("Missing refresh_token") + + try { + console.log(`>>> we are refreshing the token`) + const response = await fetch(`https://keycloak.fp.sbtp.xyz/realms/futureporn/protocol/openid-connect/token`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + client_id: configs.keycloakClientId, + client_secret: configs.keycloakClientSecret, + refresh_token: token.refresh_token, + }).toString(), + }) + + const tokensOrError = await response.json() + + if (!response.ok) throw tokensOrError + + const newTokens = tokensOrError as { + access_token: string + expires_in: number + refresh_token?: string + } + + token.access_token = newTokens.access_token + token.expires_at = Math.floor( + Date.now() / 1000 + newTokens.expires_in + ) + // Some providers only issue refresh tokens once, so preserve if we did not get a new one + if (newTokens.refresh_token) { + token.refresh_token = newTokens.refresh_token + } - token.expires_at = account.expires_at ?? 0; - token.access_token = account.access_token!; - token.refresh_token = account.refresh_token!; - } - if (profile) { - token.profile = profile + + return token + } catch (error) { + console.error("Error refreshing access_token", error) + // If we fail to refresh the token, return an error so we can handle it on the page + token.error = "RefreshTokenError" + return token + } } - - return token; }, + + + // async jwt({ token, account, profile }) { + + + + // if (account) { + // const decodedAccountToken = jwtDecode(account.access_token as any) as KeycloakProfile + // // console.log(`jwt() callback. decodedAccountToken as follows.`) + // // console.log(decodedAccountToken) + // token.client_roles = decodedAccountToken.realm_access.roles + // token.scope = decodedAccountToken.scope + // token.resource_access = decodedAccountToken.resource_access; + + // token.expires_at = account.expires_at ?? 0; + // token.access_token = account.access_token!; + // token.refresh_token = account.refresh_token!; + // } + + // if (profile) { + // token.profile = profile + // } + + // return token; + // }, async session({ session, token, trigger }) { + // console.log(`Token interceptor to add token info to the session to use on the pages. trigger=${trigger}`) // console.log(JSON.stringify(token, null, 2)) // console.log('session as follows') // console.log(JSON.stringify(session, null, 2)) + + // get user's patreon tiers and adds them to the appropriate keycloak group + const entitledTiers = await updateKeycloakUserPatreonEntitlements(token) + // console.log(`entitledTiers=${JSON.stringify(entitledTiers)}`) + const entitledTierName = getTierNameFromTierId(entitledTiers[0]) + const entitledRoles = tiersToRolesMap[entitledTierName] + console.log(`entitledRoles=${entitledRoles.join(', ')}`) + const userId = token?.sub if (!userId) throw new Error('failed to get userId from token.sub'); - // side effect which gets user's patreon tiers and adds them to the appropriate keycloak group - await updateKeycloakUserPatreonEntitlements(token) // session.account = token.account session.profile = token.profile - session.roles = token.client_roles + session.roles = entitledRoles // this is a hack to avoid the user having to log in twice to get synced with keycloak roles session.token = token - return session } diff --git a/services/next/app/lib/b2File.ts b/services/next/app/lib/b2File.ts index 68d1eb3..03bcd9b 100644 --- a/services/next/app/lib/b2File.ts +++ b/services/next/app/lib/b2File.ts @@ -2,12 +2,10 @@ import { IMeta } from "@futureporn/types"; export interface IB2File { id: number; - attributes: { - url: string; - key: string; - uploadId: string; - cdnUrl: string; - } + url: string; + key: string; + uploadId: string; + cdn_url: string; } export interface IB2FileResponse { data: IB2File; diff --git a/services/next/app/lib/constants.ts b/services/next/app/lib/constants.ts index d036fa5..50e3a52 100644 --- a/services/next/app/lib/constants.ts +++ b/services/next/app/lib/constants.ts @@ -7,7 +7,6 @@ export const siteUrl = ''+process.env.NEXT_PUBLIC_SITE_URL export const strapiUrl = ''+process.env.NEXT_PUBLIC_STRAPI_URL export const postgrestUrl = ''+process.env.NEXT_PUBLIC_POSTGREST_URL export const postgrestLocalUrl = 'http://postgrest.futureporn.svc.cluster.local:9000' -export const keycloakLocalUrl = 'http://keycloak.futureporn.svc.cluster.local:8080' export const patreonCampaignId: string = '8012692' export const patreonSupporterBenefitId: string = '4760169' export const patreonQuantumSupporterId: string = '10663202' diff --git a/services/next/app/lib/keycloak.ts b/services/next/app/lib/keycloak.ts index bf7646d..bbd6181 100644 --- a/services/next/app/lib/keycloak.ts +++ b/services/next/app/lib/keycloak.ts @@ -1,5 +1,4 @@ import { type Tier, type TiersList, tiers } from './patreon' -import { keycloakLocalUrl } from './constants' import { configs } from '../config/configs'; export interface UMAToken { @@ -34,9 +33,15 @@ export const patreonToKeycloakMap: Record = { }; +export function getTierNameFromTierId(tierId: string): string { + const tierName = Object.entries(tiers).find(([_, id]) => id === tierId)?.[0]; + if (!tierName) throw new Error(`getTierNameFromTierId failed to convert tierId=${tierId} into a tier name.`); + return tierName +} + export function getKeycloakIdFromTierId(tierId: string): string | undefined { // Find the tier name corresponding to the provided tier ID - const tierName = Object.entries(tiers).find(([_, id]) => id === tierId)?.[0]; + const tierName = getTierNameFromTierId(tierId) if (!tierName) { console.warn(`Tier ID ${tierId} not found in tiers map.`); @@ -60,7 +65,7 @@ export function getKeycloakIdFromTierId(tierId: string): string | undefined { * This service account has manage-users role so it can modify their group memebership. */ export async function getUMAToken(): Promise { - const res = await fetch(`${keycloakLocalUrl}/realms/futureporn/protocol/openid-connect/token`, { + const res = await fetch(`${configs.keycloakLocalUrl}/realms/futureporn/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' @@ -83,14 +88,16 @@ export async function getUMAToken(): Promise { export async function getUserGroups(uma: UMAToken, userId: string): Promise { - const res = await fetch(`${keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups`, { + if (!uma) throw new Error('getUserGroups() requires a UMAToken as second param, but it was undefined.'); + if (!userId) throw new Error('getUserGroups() requires a userId as second param, but it was undefined.'); + const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups`, { headers: { 'Authorization': `Bearer ${uma.access_token}` } }) const data = await res.json() as KeycloakGroup[] if (!res.ok) { - throw new Error(`failed to getUserGroups. body=${data}, res.status=${res.status}, res.statusText=${res.statusText}`); + throw new Error(`failed to getUserGroups. body=${JSON.stringify(data, null, 2)}, res.status=${res.status}, res.statusText=${res.statusText}`); } return data.map((g) => g.id) } @@ -98,7 +105,7 @@ export async function getUserGroups(uma: UMAToken, userId: string): Promise { - const res = await fetch(`${keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, { + const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${uma.access_token}` @@ -110,30 +117,40 @@ export async function addUserGroup(uma: UMAToken, userId: string, groupId: strin } -export async function deleteUserGroup(uma: UMAToken, userId: string, groupId: string): Promise { - const res = await fetch(`${keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, { +export async function removeUserGroup(uma: UMAToken, userId: string, groupId: string): Promise { + const res = await fetch(`${configs.keycloakLocalUrl}/admin/realms/futureporn/users/${userId}/groups/${groupId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${uma.access_token}` } }) if (!res.ok) { - throw new Error(`failed to deleteUserGroup. res.status=${res.status}, res.statusText=${res.statusText}`); + throw new Error(`failed to removeUserGroup. res.status=${res.status}, res.statusText=${res.statusText}`); } } - -function compareArrays(desired: T[], actual: T[]) { - const missing = desired.filter(item => !actual.includes(item)); - const extra = actual.filter(item => !desired.includes(item)); - - return { missing, extra }; +/** + * Identifies the changes required to sync the actual state with the desired state. + * + * @param desired - Array of desired group IDs. + * @param actual - Array of actual group IDs. + * @returns An object with: + * - `toAdd`: Groups in `desired` but not in `actual` (need to be added). + * - `toRemove`: Groups in `actual` but not in `desired` (need to be removed). + */ +export function calculateSyncChanges(desired: string[], actual: string[]) { + const toAdd = Array.from(new Set(desired.filter(item => !actual.includes(item)))); + const toRemove = Array.from(new Set(actual.filter(item => !desired.includes(item)))); + return { toAdd, toRemove }; } -export async function syncronizeKeycloakRoles(keycloakUserId: string, patreonTiersList: TiersList) { +export async function syncronizeKeycloakRoles(keycloakUserId: string, patreonTiersList: TiersList): Promise { + + if (!keycloakUserId) throw new Error('syncronizeKeycloakRoles() requires userId as first param, but it was undefined.'); + if (!patreonTiersList) throw new Error('patreonTiersList() requires userId as second param, but it was undefined.'); // 1. [-] get Keycloak service account uma token // POST https://keycloak.fp.sbtp.xyz/realms/futureporn/protocol/openid-connect/token @@ -148,46 +165,44 @@ export async function syncronizeKeycloakRoles(keycloakUserId: string, patreonTie // - console.log('uma token as follows') const uma = await getUMAToken() - console.log(uma) + // console.log('uma token as follows') + // console.log(uma) + // console.log('got uma token') const desiredKeycloakGroupIds = patreonTiersList .map((entitledTierId) => getKeycloakIdFromTierId(entitledTierId)) .filter((groupId): groupId is string => groupId !== undefined); - + // console.log(`got desiredKeycloakGroupIds=${desiredKeycloakGroupIds.join(', ')}`) if (desiredKeycloakGroupIds.length === 0) { throw new Error(`failed to get keycloak group id from the following patreon tiers: ${patreonTiersList.join(',')}`); } - console.log('desiredKeycloakGroupIds as follows') - console.log(desiredKeycloakGroupIds) + // console.log('desiredKeycloakGroupIds as follows') + // console.log(desiredKeycloakGroupIds) const actualKeycloakGroupIds = await getUserGroups(uma, keycloakUserId); - console.log(`actualKeyclaokGroupIds as follows`) - console.log(actualKeycloakGroupIds) + // console.log(`actualKeyclaokGroupIds as follows`) + // console.log(actualKeycloakGroupIds) + + + const { toAdd, toRemove } = calculateSyncChanges(desiredKeycloakGroupIds, actualKeycloakGroupIds); - const { missing, extra } = compareArrays(desiredKeycloakGroupIds, actualKeycloakGroupIds) - console.log(`missing=${missing.join(',')} extra=${extra.join(',')}`) + if (toAdd.length > 0) { + // console.log(`Groups to add: ${toAdd.join(', ')}`); + await Promise.all(toAdd.map(groupId => addUserGroup(uma, keycloakUserId, groupId))); + } - - if (missing) { - console.log(`Adding the following keycloak groups ${missing.join(',')}`) - const addPromises = missing.map((groupId) => addUserGroup(uma, keycloakUserId, groupId)) - await Promise.all(addPromises) - } - - - if (extra) { - console.log(`Removing the following keycloak groups ${extra.join(',')}`) - const removePromises = extra.map((groupId) => deleteUserGroup(uma, keycloakUserId, groupId)) - await Promise.all(removePromises) + if (toRemove.length > 0) { + // console.log(`Groups to remove: ${toRemove.join(', ')}`); + await Promise.all(toRemove.map(groupId => removeUserGroup(uma, keycloakUserId, groupId))); } + console.log('finished updating keycloak groups.') diff --git a/services/next/app/lib/patreon.ts b/services/next/app/lib/patreon.ts index 8faf873..2bbbd2c 100644 --- a/services/next/app/lib/patreon.ts +++ b/services/next/app/lib/patreon.ts @@ -1,11 +1,10 @@ import { postgrestLocalUrl, patreonVideoAccessBenefitId, giteaUrl } from './constants' import { type IPatron } from '@futureporn/types' import { PublicPatron } from '@futureporn/utils/patron.ts'; -import { keycloakLocalUrl, patreonApiIdentityUrl } from "./constants"; import { Session } from 'next-auth'; import { JWT } from 'next-auth/jwt'; import { type UMAToken, getKeycloakIdFromTierId, getUMAToken, syncronizeKeycloakRoles } from './keycloak'; - +import { patreonApiIdentityUrl } from './constants'; export type TiersList = string[] @@ -121,7 +120,25 @@ export const tiers = { } -export async function updateKeycloakUserPatreonEntitlements(token: JWT) { +// Maps patreon tiers to keycloak roles +// @todo this is a dirty hack, this should be removed once we figure out how to sync keycloak roles at login. +export const tiersToRolesMap: Record = { + everyone: ['cdn_2', 'default-roles-futureporn', 'offline_access', 'uma_authorization'], + free: ['cdn_2', 'default-roles-futureporn', 'offline_access', 'uma_authorization'], + archiveSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'], + stealthSupporter: ['hacker', 'cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'], + tuneItUp: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization'], + maxQ: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'], + archiveCollector: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'], + advancedArchiveSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'], + quantumSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'], + sneakyQuantumSupporter: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'], + luberPlusPlus: ['cdn_2', 'cdn_1', 'patron', 'website_shoutout', 'offline_access', 'default-roles-futureporn', 'uma_authorization', 'uploader'] +}; + + + +export async function updateKeycloakUserPatreonEntitlements(token: JWT): Promise { console.log(`updateKeycloakUserPatreonEntitlements() invoked.`) @@ -141,11 +158,11 @@ export async function updateKeycloakUserPatreonEntitlements(token: JWT) { await syncronizeKeycloakRoles(userId, patreonTiersList) - + return patreonTiersList } export async function getKeycloakIdpToken(access_token: string): Promise { - console.log(`getKeycloakIdpToken() using access_token=${access_token}`) + // console.log(`getKeycloakIdpToken() using access_token=${access_token}`) // @todo check the access_token. if it is expired, use the refresh_token to get a new access_token. diff --git a/services/next/app/lib/streams.ts b/services/next/app/lib/streams.ts index 9ae60f6..ee93f1e 100644 --- a/services/next/app/lib/streams.ts +++ b/services/next/app/lib/streams.ts @@ -14,37 +14,39 @@ const fetchStreamsOptions = { } -export async function getStreamByCuid(cuid: string): Promise { - const query = qs.stringify({ - filters: { - cuid: { - $eq: cuid - } - }, - pagination: { - limit: 1 - }, - populate: { - vtuber: { - fields: ['slug', 'displayName'] - }, - tweet: { - fields: ['isChaturbateInvite', 'isFanslyInvite', 'url'] - }, - vods: { - fields: ['note', 'cuid', 'publishedAt'], - populate: { - tagVodRelations: { - fields: ['id'] - }, - timestamps: '*' - } - } - } - }); +export async function getStreamByUUID(uuid: string): Promise { + // const query = qs.stringify({ + // filters: { + // cuid: { + // $eq: cuid + // } + // }, + // pagination: { + // limit: 1 + // }, + // populate: { + // vtuber: { + // fields: ['slug', 'displayName'] + // }, + // tweet: { + // fields: ['isChaturbateInvite', 'isFanslyInvite', 'url'] + // }, + // vods: { + // fields: ['note', 'cuid', 'publishedAt'], + // populate: { + // tagVodRelations: { + // fields: ['id'] + // }, + // timestamps: '*' + // } + // } + // } + // }); + const query = `select=*,vods(*),vtuber:vtubers(slug,display_name,id,image,image_blur)&uuid=eq.${uuid}&limit=1` const res = await fetch(`${postgrestUrl}/streams?${query}`); const json = await res.json(); - return json.data[0]; + console.log(json) + return json[0]; } export function getUrl(stream: IStream, slug: string, date: string): string { @@ -289,6 +291,7 @@ export async function getAllStreamsForVtuber(vtuberId: number, archiveStatuses = */ export async function fetchStreamData({ pageIndex, pageSize }: { pageIndex: number, pageSize: number }) { + // console.log(`fetchStreamData() invoked`) const offset = pageIndex * pageSize; // const query = qs.stringify({ // populate: { @@ -310,19 +313,19 @@ export async function fetchStreamData({ pageIndex, pageSize }: { pageIndex: numb // }, // sort: ['date:desc'] // }) - const query = 'select=*,vtuber:vtubers(*)' + const query = 'select=*,vtuber:vtubers(id,slug,image,image_blur,theme_color,display_name)' const response = await fetch( `${postgrestUrl}/streams?${query}` ); const data = await response.json(); - console.log(data) + // console.log(data) const filtered = data.filter((datum: any) => !!datum?.vtuber) const d = { rows: filtered, pageCount: Math.ceil(filtered.length / pageSize), rowCount: filtered, } - console.log(`fetchStreamData with pageIndex=${pageIndex}, pageSize=${pageSize}\n\n${JSON.stringify(d, null, 2)}`) + // console.log(`fetchStreamData with pageIndex=${pageIndex}, pageSize=${pageSize}\n\n${JSON.stringify(d, null, 2)}`) return d; } diff --git a/services/next/app/lib/vods.ts b/services/next/app/lib/vods.ts index 925a235..0be5def 100644 --- a/services/next/app/lib/vods.ts +++ b/services/next/app/lib/vods.ts @@ -24,6 +24,8 @@ export interface IVodPageProps { }; } + + export interface IVodsResponse { data: IVod[]; meta: IMeta; @@ -286,9 +288,9 @@ export async function getVod(id: number): Promise { return data; } -export async function getVods(page: number = 1, pageSize: number = 25, sortDesc = true): Promise { +export async function getVods(page: number = 1, pageSize: number = 25, sortDesc = true): Promise<{ vods: IVod[], count: number }> { // const query = qs.stringify( - // { + // { // populate: { // vtuber: { // fields: ['slug', 'displayName', 'image', 'imageBlur'] @@ -323,25 +325,36 @@ export async function getVods(page: number = 1, pageSize: number = 25, sortDesc '*', 'vtuber:vtubers(id,slug,image,display_name,image_blur)', // 'mux_asset:mux_assets(playback_id,asset_id)', - // 'thumbnail:b2_files(cdn_url)' + 'thumbnail:b2_files(cdn_url)' ] const queryObject = { select: selects.join(','), + limit: pageSize, + offset: page*pageSize }; const query = qs.stringify(queryObject, { encode: false }); const res = await fetch( `${postgrestLocalUrl}/vods?${query}`, - fetchVodsOptions + Object.assign({}, fetchVodsOptions, { headers: { + 'Prefer': 'count=exact' + }}) ); + const data = await res.json() if (!res.ok) { - const body = await res.text() - throw new Error(`Failed to fetch vods status=${res.status}, statusText=${res.statusText}, body=${body}`); + throw new Error(`Failed to fetch vods status=${res.status}, statusText=${res.statusText}, data=${data}`); + } + + console.log(`${data.length} vods. sample as follows.`) + console.log(data[0]) + // https://postgrest.org/en/latest/references/api/pagination_count.html + // HTTP/1.1 206 Partial Content + // Content-Range: 0-24/3572000 + const count = parseInt(res.headers.get('Content-Range')?.split('/').at(-1) || '0') + return { + vods: data, + count: count } - const json = await res.json() - // console.log('vods as follows') - // console.log(json) - return json; } diff --git a/services/next/app/page.tsx b/services/next/app/page.tsx index 970d1f1..f1d2031 100644 --- a/services/next/app/page.tsx +++ b/services/next/app/page.tsx @@ -14,7 +14,7 @@ import { ErrorCard } from "./components/error-card"; export default async function Page() { - const vods = await getVods(1, 9, true); + const { vods, count } = await getVods(1, 9, true); const vtubers = await getVtubers(); @@ -38,12 +38,10 @@ export default async function Page() {

Latest VODs

- + {!vods &&

Error: Failed to fetch VODs from the database

} - - {vods && vods.map((vod: IVod) => ( ))} +
See all Latest Vods diff --git a/services/next/app/profile/hooks/keycloakGroup.ts b/services/next/app/profile/hooks/keycloakGroup.ts new file mode 100644 index 0000000..3bcd1b8 --- /dev/null +++ b/services/next/app/profile/hooks/keycloakGroup.ts @@ -0,0 +1,10 @@ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { JWT } from 'next-auth/jwt' + + +const fetchKeycloakGroup = async (): Promise => { + const response = await fetch(`/api/auth/renew`) + const data = await response.json() + return data +} diff --git a/services/next/app/profile/hooks/patreonCurrentlyEntitledTiers.ts b/services/next/app/profile/hooks/patreonCurrentlyEntitledTiers.ts new file mode 100644 index 0000000..1440cda --- /dev/null +++ b/services/next/app/profile/hooks/patreonCurrentlyEntitledTiers.ts @@ -0,0 +1,16 @@ +import { TiersList } from "@/app/lib/patreon" + +export async function fetchPatreonCurrentlyEntitledTiers (): Promise { + const response = await fetch(`/api/patreon/currently-entitled-tiers`, { + credentials: 'include' + }) + const data = await response.json() + + if (!response.ok) { + throw new Error(`failed to fetchPatreonCurrentlyEntitledTiers() res.status=${response.status}`) + } + + + console.log(data) + return data +} diff --git a/services/next/app/profile/hooks/updateIsUsernamePublic.ts b/services/next/app/profile/hooks/updateIsUsernamePublic.ts deleted file mode 100644 index 1053ad7..0000000 --- a/services/next/app/profile/hooks/updateIsUsernamePublic.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type UpdateMetadataInput = { isUsernamePublic: boolean } - - -export async function updateIsUsernamePublic (data: UpdateMetadataInput): Promise { - const res = await fetch('/api/user/metadata', { - method: 'POST', - body: JSON.stringify({ - isUsernamePublic: data.isUsernamePublic - }) - }) - const d = await res.json() - if (!res.ok) throw new Error(`failed to updateIsUsernamePublic. ${res.status} ${res.statusText}`); - return d -} \ No newline at end of file diff --git a/services/next/app/profile/hooks/updateProfile.ts b/services/next/app/profile/hooks/updateProfile.ts new file mode 100644 index 0000000..3c6bac4 --- /dev/null +++ b/services/next/app/profile/hooks/updateProfile.ts @@ -0,0 +1,21 @@ +import { configs } from "@/app/config/configs"; + + +export type UpdateIsUsernamePublicInput = { + isUsernamePublic: boolean; + access_token: string; +} + + +export async function updateIsUsernamePublic (data: UpdateIsUsernamePublicInput): Promise { + console.log(`upadateIsUernamePublic configs.nextUrl=${configs.nextUrl}`) + const res = await fetch(`${configs.nextUrl}/api/profile`, { + method: 'POST', + body: JSON.stringify({ + username_visibility: data.isUsernamePublic + }) + }) + const d = await res.json() + if (!res.ok) throw new Error(`failed to updateProfile. ${res.status} ${res.statusText}`); + return d +} \ No newline at end of file diff --git a/services/next/app/profile/hooks/useMetadata.ts b/services/next/app/profile/hooks/useMetadata.ts index 09049a2..62149e9 100644 --- a/services/next/app/profile/hooks/useMetadata.ts +++ b/services/next/app/profile/hooks/useMetadata.ts @@ -1,52 +1,52 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +// import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -export type MetadataPayload = { - metadata: { - first_name: string - last_name: string - patreon: { - entitledTiers: string[], - lifetimeSupportCents: number, - currentlyEntitledSupportCents: number - }, - isUsernamePublic: boolean - } -} -export type UpdateMetadataInput = { isUsernamePublic: boolean } +// export type MetadataPayload = { +// metadata: { +// first_name: string +// last_name: string +// patreon: { +// entitledTiers: string[], +// lifetimeSupportCents: number, +// currentlyEntitledSupportCents: number +// }, +// isUsernamePublic: boolean +// } +// } +// export type UpdateMetadataInput = { isUsernamePublic: boolean } -const fetchMetadata = async (): Promise => { - const response = await fetch(`/api/user/metadata`) - const data = await response.json() - return data -} +// const fetchMetadata = async (): Promise => { +// const response = await fetch(`/api/user/attributes`) +// const data = await response.json() +// return data +// } -const fetchMutateMetadata = async (data: UpdateMetadataInput): Promise => { - const res = await fetch('/api/user/metadata', { - method: 'POST', - body: JSON.stringify({ - isUsernamePublic: data.isUsernamePublic - }) - }) - if (!res.ok) throw new Error(`failed to fetchMutateMetadata. ${res.status} ${res.statusText}`); -} +// const fetchMutateMetadata = async (data: UpdateMetadataInput): Promise => { +// const res = await fetch('/api/user/attributes', { +// method: 'POST', +// body: JSON.stringify({ +// isUsernamePublic: data.isUsernamePublic +// }) +// }) +// if (!res.ok) throw new Error(`failed to fetchMutateMetadata. ${res.status} ${res.statusText}`); +// } -const useMetadata = () => { - return useQuery({ - queryKey: ['user/metadata'], - queryFn: () => fetchMetadata() - }) -} +// const useMetadata = () => { +// return useQuery({ +// queryKey: ['user/attributes'], +// queryFn: () => fetchMetadata() +// }) +// } -const useMutateMetadata = () => { - const queryClient = useQueryClient() +// const useMutateMetadata = () => { +// const queryClient = useQueryClient() - return useMutation({ - mutationFn: (data: UpdateMetadataInput) => fetchMutateMetadata(data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['user/metadata'] }) - } - }) -} +// return useMutation({ +// mutationFn: (data: UpdateMetadataInput) => fetchMutateMetadata(data), +// onSuccess: () => { +// queryClient.invalidateQueries({ queryKey: ['user/attributes'] }) +// } +// }) +// } -export { useMetadata, fetchMetadata, useMutateMetadata } +// export { useMetadata, fetchMetadata, useMutateMetadata } diff --git a/services/next/app/profile/page.tsx b/services/next/app/profile/page.tsx index 291afc9..0a54f91 100644 --- a/services/next/app/profile/page.tsx +++ b/services/next/app/profile/page.tsx @@ -4,12 +4,19 @@ import 'react-loading-skeleton/dist/skeleton.css'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner, faCheckCircle, faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { updateIsUsernamePublic } from "./hooks/updateIsUsernamePublic"; +// import { updateIsUsernamePublic } from "./hooks/updateProfile"; +import { useQuery } from '@tanstack/react-query'; import ProtectedRoute from "../components/protected-route"; import VibrateTest from "../components/vibrate-test"; import { useSession } from "next-auth/react" import { LoginButton, LogoutButton } from "../components/auth-buttons"; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import Link from 'next/link'; +import { useEffect } from 'react'; +import { useState } from 'react'; +import { fetchPatreonCurrentlyEntitledTiers } from './hooks/patreonCurrentlyEntitledTiers'; +import { Spinner } from '../components/spinner'; +import { signIn, signOut } from "next-auth/react"; export default function Page() { @@ -18,13 +25,29 @@ export default function Page() { // if (sessionContext.loading) { // return // } - const session = useSession() + const { data, status, update } = useSession() - const metadataMutation = useMutation({ - mutationFn: updateIsUsernamePublic, - mutationKey: ['user/metadata'], - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user/metadata'] }) - }) + + async function renewJwt() { + await signOut({ redirect: false }); + await signIn("keycloak", { redirect: false }); + } + + // const { data: patreonTier, isPending, refetch } = useQuery({ + // queryKey: ['user/tier'], + // queryFn: () => fetchPatreonCurrentlyEntitledTiers(), + // }) + + // const keycloakGroupQuery = useQuery({ + // queryKey: ['user/group'], + // queryFn: () => fetchKeycloakGroup() + // }) + + // const attributesMutation = useMutation({ + // mutationFn: updateIsUsernamePublic, + // mutationKey: ['user/attributes'], + // onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user/attributes'] }) + // }) // const metadataQuery = useQuery({ // queryKey: ['user/metadata'], @@ -40,8 +63,36 @@ export default function Page() { // } // }, [metadataQuery.data]) - const isUsernamePublic = (session.data?.profile?.username_visibility === 'public') + const isUsernamePublic = (data?.profile?.username_visibility === 'public') + const [isGo, setIsGo] = useState() + // useEffect(() => { + // const fetchData = async () => { + // const res = await fetch(`${process.env.KEYCLOAK_URL}/protocol/openid-connect/token`, { + // method: 'POST', + // headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + // body: new URLSearchParams({ + // client_id: process.env.KEYCLOAK_CLIENT_ID, + // client_secret: process.env.KEYCLOAK_CLIENT_SECRET, + // grant_type: 'refresh_token', + // refresh_token: session.refreshToken, + // }), + // }); + + // const data = await res.json(); + + // if (res.ok) { + // // Update session with new token + // session.accessToken = data.access_token; + // session.refreshToken = data.refresh_token; + // res.status(200).json(session); + // } else { + // res.status(400).json({ error: 'Token refresh failed' }); + // } + // } + + // fetchData(); + // }, [isGo]); return ( @@ -49,16 +100,20 @@ export default function Page() { {/* {(metadataQuery.isPending) && } */}

Debug section

- {/* {(metadataQuery.isPending) ? isPending=true : isPending=false} */} + {/* {(isPending) ? Syncronizing Patron entitlements... : Patreon entitlements synced.} */} {/*

metadataQuery as follows

*/} {/*
{JSON.stringify(metadataQuery.data, null, 2)}
*/}

session as follows

-
{JSON.stringify(session, null, 2)}
- {(session.status === 'unauthenticated') && } +
{JSON.stringify(data, null, 2)}
+ {(status === 'unauthenticated') && } + + {/* */} + +
- {(session.data?.user) && + {(data?.user) &&

Profile

@@ -69,11 +124,16 @@ export default function Page() {
{/*

{username}

*/} -

{session.data?.user?.name}

+

{data?.user?.name}

+ {/*

Patronage

+
+

You belong to the {patreonTier} tier. Thank you!

+
*/} +

Patron Perks

Test Perk (not real)

@@ -85,11 +145,9 @@ export default function Page() {

Website Shoutout

- Display {(!!session.data.user.name) ? session.data.user.name : } publicly? - metadataMutation.mutate({ isUsernamePublic: evt.target.checked })}/> - {(metadataMutation.status === 'success') && } - {(metadataMutation.status === 'pending') && } - {(metadataMutation.status === 'error') && } + {isUsernamePublic && {data.user.name} will be displayed on the /patrons page} + {!isUsernamePublic && Your username will be hidden from the /patrons page due to your privacy setting.} + {/* Users can update privacy pref at https://keycloak.fp.sbtp.xyz/realms/futureporn/account/ */}
@@ -117,6 +175,7 @@ export default function Page() { }
) + // if (metadataQuery.isPending) return ; // return
{JSON.stringify(metadataQuery.data, null, 2)}
// return

@todo

@@ -160,10 +219,10 @@ export default function Page() { // //
// Display {(!!username) ? username : } publicly? - // metadataMutation.mutate({ isUsernamePublic: evt.target.checked })}/> - // {(metadataMutation.status === 'success') && } - // {(metadataMutation.status === 'pending') && } - // {(metadataMutation.status === 'error') && } + // attributesMutation.mutate({ isUsernamePublic: evt.target.checked })}/> + // {(attributesMutation.status === 'success') && } + // {(attributesMutation.status === 'pending') && } + // {(attributesMutation.status === 'error') && } //
//
diff --git a/services/next/middleware.ts b/services/next/middleware.ts index 01ab6e7..1cc033e 100644 --- a/services/next/middleware.ts +++ b/services/next/middleware.ts @@ -24,7 +24,7 @@ import { configs } from "./app/config/configs"; export const tokenRefreshBufferSeconds = 300; export const isSessionSecure = configs.nextAuthUrl.startsWith("https://"); -export const SESSION_COOKIE = isSessionSecure ? "__Secure-next-auth.session-token" : "next-auth.session-token"; +export const sessionCookieName = isSessionSecure ? "__Secure-next-auth.session-token" : "next-auth.session-token"; // export const sessionTimeout = 60 * 60 * 24 * 30; // 30 days export const sessionTimeout = 60 * 5 export const signinSubUrl = "/api/auth/signin"; @@ -68,8 +68,7 @@ export async function refreshAccessToken(token: JWT): Promise { expires_at: newAccessToken?.expires_in + timeInSeconds, refresh_token: newAccessToken?.refresh_token ?? token?.refresh_token }; - console.log('newToken as follows') - console.log(newToken) + console.log('refreshAccessToken() succeeded.') return newToken } catch (e) { @@ -88,81 +87,102 @@ export async function refreshAccessToken(token: JWT): Promise { */ export function shouldUpdateToken(token: JWT): boolean { const timeInSeconds = Math.floor(Date.now() / 1000); - return timeInSeconds >= (token?.expires_at - tokenRefreshBufferSeconds); + const timeRemaining = token?.expires_at - timeInSeconds; // Calculate time remaining + console.log(`shouldUpdateToken() -- access_token -- ${getTokenExpiryStatus(token.expires_at)}`); + return timeRemaining <= tokenRefreshBufferSeconds; // Should refresh if within buffer } export function updateCookie( - sessionToken: string | null, - request: NextRequest, - response: NextResponse + sessionToken: string | null, + request: NextRequest, + response: NextResponse ): NextResponse { - /* - * BASIC IDEA: - * - * 1. Set request cookies for the incoming getServerSession to read new session - * 2. Updated request cookie can only be passed to server if it's passed down here after setting its updates - * 3. Set response cookies to send back to browser - */ + /* + * BASIC IDEA: + * + * 1. Set request cookies for the incoming getServerSession to read new session + * 2. Updated request cookie can only be passed to server if it's passed down here after setting its updates + * 3. Set response cookies to send back to browser + */ - if (sessionToken) { - // console.log('Set the session token in the request and response cookies for a valid session') - // request.cookies.set(SESSION_COOKIE, sessionToken); - response = NextResponse.next({ - request: { - headers: request.headers - } - }); - response.cookies.set(SESSION_COOKIE, sessionToken, { - httpOnly: true, - maxAge: sessionTimeout, - secure: isSessionSecure, - sameSite: "lax" - }); + if (sessionToken) { + // Set the session token in the request and response cookies for a valid session + request.cookies.set(sessionCookieName, sessionToken); + response = NextResponse.next({ + request: { + headers: request.headers + } + }); + response.cookies.set(sessionCookieName, sessionToken, { + httpOnly: true, + maxAge: sessionTimeout, + secure: isSessionSecure, + sameSite: "lax" + }); + } else { + request.cookies.delete(sessionCookieName); + return NextResponse.redirect(new URL(signinSubUrl, request.url)); + } + + return response; +} + +function getTokenExpiryStatus(expiryDate: number): string { + const now = Date.now(); // Current time in milliseconds + + // Convert expiryDate (which is in seconds) to milliseconds + const expiryTime = expiryDate * 1000; + + const differenceInSeconds = Math.floor((expiryTime - now) / 1000); // Difference in seconds + + if (differenceInSeconds > 0) { + return `Token will expire in ${differenceInSeconds} seconds (${expiryDate})`; + } else if (differenceInSeconds < 0) { + return `Token expired ${Math.abs(differenceInSeconds)} seconds ago (${expiryDate})`; } else { - request.cookies.delete(SESSION_COOKIE); - return NextResponse.redirect(new URL(signinSubUrl, request.url)); + return "Token is expiring now"; // Edge case when the token expires exactly at the current time } - - return response; } -// @todo this is broken. This sets multiple cookies with the same name. // @todo this is broken. The updated session token does not get used! export const middleware: NextMiddleware = async (request: NextRequest) => { - + const path = request.nextUrl.pathname; const token = await getToken({ req: request }); - // console.log(`middlew.are. token as follows`) - // console.log(token) let response = NextResponse.next(); - // return response + return response + + console.log(`middleware on path=${path}`) + response.cookies.set('cookie-crisp', 'yummy-cereal-yum-yum!'); + if (!token) { - return response + return response; } - // we only want to consider updating the token if the request is not an auth request - if (request.nextUrl.pathname.startsWith('/api/auth')) { - return response + if (path.startsWith('/api/auth')) { + return response; } - // we only want to consider updating the token if the request is accessing /api/patreon/* endpoint - if (!request.nextUrl.pathname.startsWith('/api/patreon')) { - return response - } + if (shouldUpdateToken(token)) { console.log(`Attempting to update the token.`) - try { + console.log(`middleware.ts before refreshAccessToken() we are looking at token -- ${getTokenExpiryStatus(token.expires_at)}`) + const newToken = await refreshAccessToken(token) + console.log(`middleware.ts after refreshAccessToken() -- ${getTokenExpiryStatus(newToken.expires_at)}`) + // console.log(JSON.stringify(newToken)) const newSessionToken = await encode({ secret: configs.nextAuthSecret, - token: await refreshAccessToken(token), + token: newToken, maxAge: sessionTimeout }); + console.log('newSessionToken as follows') + console.log(newSessionToken) response = updateCookie(newSessionToken, request, response); } catch (error) { console.log("Error refreshing token: ", error); @@ -171,6 +191,18 @@ export const middleware: NextMiddleware = async (request: NextRequest) => { } console.log(`We succeeded in updating the token.`) - // console.log(response) + + return response; -}; \ No newline at end of file +}; + + +export const config = { + matcher: [ + /* + * Match all request paths that start with: + * - /api/patreon/ + */ + '/api/patreon/:path*', + ], +} \ No newline at end of file