Compare commits

...

3 Commits

Author SHA1 Message Date
jubnl 249ab217f8 feature: db agnostic + base tables 2026-04-26 23:22:06 +02:00
Julien G. 499097fa3c align dev (#899)
* chore: bump version to 3.0.0 [skip ci]

* fix: resolve dead wiki links across install and config pages

* fix(reservations): restore correct day assignment for non-transport bookings

v3.0.0 switched the planner from rendering reservations by
reservation_time to rendering them by day_id (commit 3f61e1c), but
migration 110 only backfilled day_id for transport types. Tours,
restaurants, events and 'other' bookings kept whatever day_id was
stored in the DB — often the trip's first day, from older code paths
that defaulted it there — so after the upgrade those rows all show
up on day 1 regardless of their actual reservation_time.

- Migration 122: for every non-hotel reservation, null out any
  day_id / end_day_id that does not match the reservation's time,
  then backfill it from reservation_time / reservation_end_time.
  Idempotent; leaves already-correct rows alone.
- reservationService.createReservation / updateReservation now
  derive day_id / end_day_id from reservation_time /
  reservation_end_time when the client didn't send one explicitly,
  so the mismatch cannot reappear on new or edited bookings.
  Hotels are skipped because they store their date range on the
  linked day_accommodation.

* chore: bump version to 3.0.1 [skip ci]

* fix(oidc): normalize discovery doc issuer before comparison

Trailing slash in doc.issuer (e.g. Authentik) caused a mismatch against
the already-normalized configured issuer, breaking OIDC login entirely.

Closes #834

* test(systemNotices): exclude v3 upgrade notices from login_count-only tests

Tests that expect an empty notice list were using first_seen_version='0.0.0'
(DB default), which matches the existingUserBeforeVersion('3.0.0') condition
now that the app is at 3.0.1. Set first_seen_version='3.0.0' so only the
firstLogin condition controls visibility in these tests.

* chore: bump version to 3.0.2 [skip ci]

* fix(oidc): normalize id_token iss claim before issuer comparison (#837)

jwt.verify does an exact string match on the issuer. Providers like
Authentik include a trailing slash in the id_token iss claim while the
configured issuer is already normalized (no trailing slash), causing
every login attempt to fail with jwt issuer invalid.

Move the issuer check out of jwt.verify options and apply the same
trailing-slash normalization used in the discovery doc validation.
Also adds OIDC-SVC-033–036 unit tests covering exact match, trailing
slash, wrong issuer, and wrong audience cases.

Closes #834

* chore: bump version to 3.0.3 [skip ci]

* fix(oidc,ui): restore Authentik login and fix mobile delete dialog (#845)

OIDC: when OIDC_DISCOVERY_URL is explicitly set, trust the discovery
doc's issuer for id_token comparison instead of rejecting a path
mismatch as an error. Authentik (and similar realm-path providers)
return a canonical issuer like /application/o/<slug>/ that differs
from the operator's base OIDC_ISSUER. Strict equality blocked login
in 3.x despite working in v2. Default discovery (no custom URL) keeps
the strict check. Adds OIDC-SVC-037/038/039.

UI: ConfirmDialog and CopyTripDialog lacked the --bottom-nav-h
paddingBottom offset that other overlays already use. On mobile portrait
the action buttons were hidden behind the sticky bottom nav bar.

Closes #843
Closes #844

* chore: bump version to 3.0.4 [skip ci]

* fix(files): open attachments only in new tab (#840)

window.open with noreferrer returns null, which triggered the popup-blocked download fallback in addition to the new-tab open. Use a target=_blank anchor click instead.

* chore: bump version to 3.0.5 [skip ci]

* fix(journey,pdf): journey reorder sort_order + PDF multi-day transport (#848)

* fix(journey): make sort_order authoritative for within-day entry ordering

Reorder buttons appeared broken because the server ORDER BY put entry_time
before sort_order, so entries synced from trip places with differing times
would always sort by time regardless of sort_order writes. The client store
mirrored the same comparator, making even the optimistic update invisible.

- Change ORDER BY to (entry_date, sort_order, id) in getJourneyFull and listEntries
- Fix syncTripPlaces and onPlaceCreated to assign MAX+1 sort_order per day instead of day_number/0
- Update client store comparator to match
- Add DB migration to backfill sort_order using old effective key (entry_time, id) so existing journeys retain their visual order
- Add tests: JOURNEY-SVC-089–093, FE-STORE-JOURNEY-018–019

Closes #846

* fix(pdf): include multi-day transport return/arrival in PDF itinerary (#847)

Reservations were matched to days by pickup date only, so the end-day
card (e.g. car Return, flight Arrival) was silently dropped from the PDF.
Add span-aware helpers mirroring DayPlanSidebar logic: match by day_id/end_day_id
span, show reservation_end_time on end days, prefix title with phase label
(Return/Arrival/etc.), and use per-day position for sort order.

* test(pdf): add missing day_id to transport reservation fixture

* chore: bump version to 3.0.6 [skip ci]

* [Snyk] Security upgrade uuid from 9.0.1 to 14.0.0 (#849)

* fix: server/package.json & server/package-lock.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UUID-16133035

* fix: bump fast-xml-parser version

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: jubnl <jgunther021@gmail.com>

* chore: bump version to 3.0.7 [skip ci]

* fix: hot fixes 23-04-2026 (#856)

* fix(packing): resolve avatar URL path in bag and category assignees (#854)

packingService was returning raw avatar filenames from the DB instead of
the full /uploads/avatars/<filename> path, causing broken profile images
for users with uploaded avatars.

* fix(budget): use Map.get() to fix category rename no-op (#855)

* fix(security): relax Referrer-Policy and document HSTS_INCLUDE_SUBDOMAINS (#862) (#863)

- Change Helmet default from no-referrer to strict-origin-when-cross-origin
  so browsers send the origin on cross-origin requests, allowing Google Maps
  API key restrictions by HTTP referrer to work correctly
- Document HSTS_INCLUDE_SUBDOMAINS in all deployment artifacts:
  .env.example, docker-compose.yml, README.md, unraid-template.xml,
  charts/values.yaml, charts/configmap.yaml, wiki/Environment-Variables.md

* fix(planner): prefetch budget items on trip page mount (#864)

Loads budgetItems alongside reservations when TripPlannerPage mounts so
the Budget category dropdown in ReservationModal and TransportModal shows
pre-existing categories on first open, regardless of whether the Budget
tab has been visited.

Closes #861

* fix(reservations): prevent Invalid Date when end time is set without end date (#866)

When reservation_end_time held a bare time string ("HH:MM"), fmtDate()
produced Invalid Date on the reservation card.

- Modal: when end date is blank but end time is filled, construct a
  same-day ISO datetime using the start date (prevents time-only strings
  from ever being persisted)
- Panel: derive endDatePart via regex so date-only end values ("YYYY-MM-DD")
  still show the multi-day range, while bare time strings are skipped and
  handled correctly by the existing time column logic

Closes #860

* fix(planner): format reservation end time instead of rendering raw ISO string (#867)

Closes #859

* fix(planner): wire Route toggle into mobile day sidebar (#850) (#868)

The per-booking Route icon was missing on mobile because the mobile
DayPlanSidebar invocation in TripPlannerPage didn't pass
visibleConnectionIds or onToggleConnection. Mobile PWA users couldn't
activate reservation map overlays without forcing desktop mode.

Also corrects the Map-Features wiki: fixes the setting name
("Booking route labels" not "Show connection labels"), documents the
route_calculation requirement for travel-time pills, and explains that
overlays are off by default and must be toggled per reservation.

* chore: bump version to 3.0.8 [skip ci]

* docs(wiki): add MCP OAuth troubleshooting entry for missing APP_URL

* Fix demo banner overlapping bottom tab bar on mobile

The demo welcome modal extended below the mobile bottom tab bar,
hiding the dismiss button so visitors couldn't close it.

- Use dvh so mobile URL bar is accounted for correctly
- Reserve ~80px of bottom padding for the tab bar
- Make the footer sticky so the dismiss button stays visible
  while scrolling through the modal content
- Bump z-index to ensure the overlay sits above the tab bar

* Fix 500 on reservation edit after DB reinit (issue #883)

saveEndpoints was bound at module load via db.transaction(...). When the
demo-mode hourly reset (or a self-hoster's backup restore) closes the DB
connection and reinitialises it, the bound transaction still references
the now-closed connection — every subsequent reservation save with an
endpoints field throws "The database connection is not open", which the
client surfaces as "Internal server error".

Bind the transaction lazily on each call so it always runs against the
current connection.

* Fix exit code 132 on old CPUs by replacing sharp with jimp (issue #888) (#895)

sharp's prebuilt Linux x64 binary requires SSE4.2 (x86-64-v2), causing a
SIGILL crash on older hardware (e.g. AMD A6-3420M). Replace with jimp, a
pure-JS image library with no native binaries. Also skip thumbnail generation
entirely when the Journey addon is disabled (the default), preventing the
issue for most installs regardless of the image library used.

* chore: Add Trademark policy

* chore: Add Trademark policy

* chore: bump version to 3.0.9 [skip ci]

---------

Co-authored-by: Maurice <61554723+mauriceboe@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Maurice <mauriceboe@icloud.com>
Co-authored-by: Xre0uS <36565320+Xre0uS@users.noreply.github.com>
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2026-04-26 23:18:22 +02:00
Julien G. 002ea91be8 Align dev (#869)
* chore: bump version to 3.0.0 [skip ci]

* fix: resolve dead wiki links across install and config pages

* fix(reservations): restore correct day assignment for non-transport bookings

v3.0.0 switched the planner from rendering reservations by
reservation_time to rendering them by day_id (commit 3f61e1c), but
migration 110 only backfilled day_id for transport types. Tours,
restaurants, events and 'other' bookings kept whatever day_id was
stored in the DB — often the trip's first day, from older code paths
that defaulted it there — so after the upgrade those rows all show
up on day 1 regardless of their actual reservation_time.

- Migration 122: for every non-hotel reservation, null out any
  day_id / end_day_id that does not match the reservation's time,
  then backfill it from reservation_time / reservation_end_time.
  Idempotent; leaves already-correct rows alone.
- reservationService.createReservation / updateReservation now
  derive day_id / end_day_id from reservation_time /
  reservation_end_time when the client didn't send one explicitly,
  so the mismatch cannot reappear on new or edited bookings.
  Hotels are skipped because they store their date range on the
  linked day_accommodation.

* chore: bump version to 3.0.1 [skip ci]

* fix(oidc): normalize discovery doc issuer before comparison

Trailing slash in doc.issuer (e.g. Authentik) caused a mismatch against
the already-normalized configured issuer, breaking OIDC login entirely.

Closes #834

* test(systemNotices): exclude v3 upgrade notices from login_count-only tests

Tests that expect an empty notice list were using first_seen_version='0.0.0'
(DB default), which matches the existingUserBeforeVersion('3.0.0') condition
now that the app is at 3.0.1. Set first_seen_version='3.0.0' so only the
firstLogin condition controls visibility in these tests.

* chore: bump version to 3.0.2 [skip ci]

* fix(oidc): normalize id_token iss claim before issuer comparison (#837)

jwt.verify does an exact string match on the issuer. Providers like
Authentik include a trailing slash in the id_token iss claim while the
configured issuer is already normalized (no trailing slash), causing
every login attempt to fail with jwt issuer invalid.

Move the issuer check out of jwt.verify options and apply the same
trailing-slash normalization used in the discovery doc validation.
Also adds OIDC-SVC-033–036 unit tests covering exact match, trailing
slash, wrong issuer, and wrong audience cases.

Closes #834

* chore: bump version to 3.0.3 [skip ci]

* fix(oidc,ui): restore Authentik login and fix mobile delete dialog (#845)

OIDC: when OIDC_DISCOVERY_URL is explicitly set, trust the discovery
doc's issuer for id_token comparison instead of rejecting a path
mismatch as an error. Authentik (and similar realm-path providers)
return a canonical issuer like /application/o/<slug>/ that differs
from the operator's base OIDC_ISSUER. Strict equality blocked login
in 3.x despite working in v2. Default discovery (no custom URL) keeps
the strict check. Adds OIDC-SVC-037/038/039.

UI: ConfirmDialog and CopyTripDialog lacked the --bottom-nav-h
paddingBottom offset that other overlays already use. On mobile portrait
the action buttons were hidden behind the sticky bottom nav bar.

Closes #843
Closes #844

* chore: bump version to 3.0.4 [skip ci]

* fix(files): open attachments only in new tab (#840)

window.open with noreferrer returns null, which triggered the popup-blocked download fallback in addition to the new-tab open. Use a target=_blank anchor click instead.

* chore: bump version to 3.0.5 [skip ci]

* fix(journey,pdf): journey reorder sort_order + PDF multi-day transport (#848)

* fix(journey): make sort_order authoritative for within-day entry ordering

Reorder buttons appeared broken because the server ORDER BY put entry_time
before sort_order, so entries synced from trip places with differing times
would always sort by time regardless of sort_order writes. The client store
mirrored the same comparator, making even the optimistic update invisible.

- Change ORDER BY to (entry_date, sort_order, id) in getJourneyFull and listEntries
- Fix syncTripPlaces and onPlaceCreated to assign MAX+1 sort_order per day instead of day_number/0
- Update client store comparator to match
- Add DB migration to backfill sort_order using old effective key (entry_time, id) so existing journeys retain their visual order
- Add tests: JOURNEY-SVC-089–093, FE-STORE-JOURNEY-018–019

Closes #846

* fix(pdf): include multi-day transport return/arrival in PDF itinerary (#847)

Reservations were matched to days by pickup date only, so the end-day
card (e.g. car Return, flight Arrival) was silently dropped from the PDF.
Add span-aware helpers mirroring DayPlanSidebar logic: match by day_id/end_day_id
span, show reservation_end_time on end days, prefix title with phase label
(Return/Arrival/etc.), and use per-day position for sort order.

* test(pdf): add missing day_id to transport reservation fixture

* chore: bump version to 3.0.6 [skip ci]

* [Snyk] Security upgrade uuid from 9.0.1 to 14.0.0 (#849)

* fix: server/package.json & server/package-lock.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UUID-16133035

* fix: bump fast-xml-parser version

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: jubnl <jgunther021@gmail.com>

* chore: bump version to 3.0.7 [skip ci]

* fix: hot fixes 23-04-2026 (#856)

* fix(packing): resolve avatar URL path in bag and category assignees (#854)

packingService was returning raw avatar filenames from the DB instead of
the full /uploads/avatars/<filename> path, causing broken profile images
for users with uploaded avatars.

* fix(budget): use Map.get() to fix category rename no-op (#855)

* fix(security): relax Referrer-Policy and document HSTS_INCLUDE_SUBDOMAINS (#862) (#863)

- Change Helmet default from no-referrer to strict-origin-when-cross-origin
  so browsers send the origin on cross-origin requests, allowing Google Maps
  API key restrictions by HTTP referrer to work correctly
- Document HSTS_INCLUDE_SUBDOMAINS in all deployment artifacts:
  .env.example, docker-compose.yml, README.md, unraid-template.xml,
  charts/values.yaml, charts/configmap.yaml, wiki/Environment-Variables.md

* fix(planner): prefetch budget items on trip page mount (#864)

Loads budgetItems alongside reservations when TripPlannerPage mounts so
the Budget category dropdown in ReservationModal and TransportModal shows
pre-existing categories on first open, regardless of whether the Budget
tab has been visited.

Closes #861

* fix(reservations): prevent Invalid Date when end time is set without end date (#866)

When reservation_end_time held a bare time string ("HH:MM"), fmtDate()
produced Invalid Date on the reservation card.

- Modal: when end date is blank but end time is filled, construct a
  same-day ISO datetime using the start date (prevents time-only strings
  from ever being persisted)
- Panel: derive endDatePart via regex so date-only end values ("YYYY-MM-DD")
  still show the multi-day range, while bare time strings are skipped and
  handled correctly by the existing time column logic

Closes #860

* fix(planner): format reservation end time instead of rendering raw ISO string (#867)

Closes #859

* fix(planner): wire Route toggle into mobile day sidebar (#850) (#868)

The per-booking Route icon was missing on mobile because the mobile
DayPlanSidebar invocation in TripPlannerPage didn't pass
visibleConnectionIds or onToggleConnection. Mobile PWA users couldn't
activate reservation map overlays without forcing desktop mode.

Also corrects the Map-Features wiki: fixes the setting name
("Booking route labels" not "Show connection labels"), documents the
route_calculation requirement for travel-time pills, and explains that
overlays are off by default and must be toggled per reservation.

* chore: bump version to 3.0.8 [skip ci]

---------

Co-authored-by: Maurice <61554723+mauriceboe@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Maurice <mauriceboe@icloud.com>
Co-authored-by: Xre0uS <36565320+Xre0uS@users.noreply.github.com>
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2026-04-23 19:57:50 +02:00
284 changed files with 192117 additions and 733 deletions
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
- name: Publish to GitHub wiki - name: Publish to GitHub wiki
uses: Andrew-Chen-Wang/github-wiki-action@v5 uses: Andrew-Chen-Wang/github-wiki-action@v5
with: with:
strategy: init strategy: clone
+1
View File
@@ -400,6 +400,7 @@ Caddy handles TLS and WebSockets automatically.
| `DEFAULT_LANGUAGE` | Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: `de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar` | `en` | | `DEFAULT_LANGUAGE` | Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: `de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar` | `en` |
| `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin | | `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin |
| `FORCE_HTTPS` | Optional. When `true`: 301-redirects HTTP to HTTPS, sends HSTS, adds CSP `upgrade-insecure-requests`, forces the session cookie `secure` flag. Useful behind a TLS-terminating reverse proxy. Requires `TRUST_PROXY`. | `false` | | `FORCE_HTTPS` | Optional. When `true`: 301-redirects HTTP to HTTPS, sends HSTS, adds CSP `upgrade-insecure-requests`, forces the session cookie `secure` flag. Useful behind a TLS-terminating reverse proxy. Requires `TRUST_PROXY`. | `false` |
| `HSTS_INCLUDE_SUBDOMAINS` | When `true`: adds the `includeSubDomains` directive to the HSTS header, extending HTTPS enforcement to all subdomains. Only effective when HSTS is active (`FORCE_HTTPS=true` or `NODE_ENV=production`). Leave `false` if you run other services on sibling subdomains over plain HTTP. | `false` |
| `COOKIE_SECURE` | Controls the `secure` flag on the `trek_session` cookie. Auto-derived: on when `NODE_ENV=production` or `FORCE_HTTPS=true`. Escape hatch: set `false` to allow session cookies over plain HTTP. Not recommended in production. | auto | | `COOKIE_SECURE` | Controls the `secure` flag on the `trek_session` cookie. Auto-derived: on when `NODE_ENV=production` or `FORCE_HTTPS=true`. Escape hatch: set `false` to allow session cookies over plain HTTP. Not recommended in production. | auto |
| `TRUST_PROXY` | Number of trusted reverse proxies. Tells Express to read client IP from `X-Forwarded-For` and protocol from `X-Forwarded-Proto`. Defaults to `1` in production; off in dev unless set. | `1` | | `TRUST_PROXY` | Number of trusted reverse proxies. Tells Express to read client IP from `X-Forwarded-For` and protocol from `X-Forwarded-Proto`. Defaults to `1` in production; off in dev unless set. | `1` |
| `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IPs (e.g. Immich on your LAN). Loopback and link-local addresses remain blocked. | `false` | | `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IPs (e.g. Immich on your LAN). Loopback and link-local addresses remain blocked. | `false` |
+121
View File
@@ -0,0 +1,121 @@
# Trademark Policy
## Introduction
This is the TREK project's policy for the use of our trademarks. While TREK is
available under the GNU Affero General Public License v3.0 (AGPL-3.0), that
license does not include a license to use our trademarks.
This policy describes how you may use our trademarks. Our goal is to strike a
balance between: 1) our need to ensure that our trademarks remain reliable
indicators of the software we release; and 2) our community members' desire to
be full participants in the TREK project.
## Our trademarks
This policy covers the name "TREK" as well as any associated logos, trade dress,
goodwill, or designs (our "Marks").
## In general
Whenever you use our Marks, you must always do so in a way that does not mislead
anyone about exactly who is the source of the software. For example, you cannot
say you are distributing TREK when you're distributing a modified version of it,
because people would think they would be getting the same software that they
can get directly from us when they aren't. You also cannot use our Marks on
your website in a way that suggests that your website is an official TREK
website or that we endorse your website. But, if true, you can say you like
TREK, that you participate in the TREK community, that you are providing an
unmodified version of TREK, or that you wrote a guide describing how to use
TREK.
This fundamental requirement, that it is always clear to people what they are
getting and from whom, is reflected throughout this policy. It should also
serve as your guide if you are not sure about how you are using the Marks.
In addition:
* You may not use or register, in whole or in part, the Marks as part of your
own trademark, service mark, domain name, company name, trade name, product
name or service name.
* Trademark law does not allow your use of names or trademarks that are too
similar to ours. You therefore may not use an obvious variation of any of our
Marks or any phonetic equivalent, foreign language equivalent, takeoff, or
abbreviation for a similar or compatible product or service.
* You agree that you will not acquire any rights in the Marks and that any
goodwill generated by your use of the Marks and participation in our
community inures solely to our benefit.
## Distribution of unmodified source code or unmodified executable code we have compiled
When you redistribute an unmodified copy of TREK, you are not changing the
quality or nature of it. Therefore, you may retain the Marks we have placed on
the software to identify your redistribution. This kind of use only applies if
you are redistributing an official TREK distribution that has not been changed
in any way.
## Distribution of executable code that you have compiled, or modified code
You may use the word mark "TREK", but not any TREK logos, to truthfully
describe the origin of the software that you are providing, that is, that the
code you are distributing is a modification of TREK. You may say, for example,
that "this software is derived from the source code for TREK."
Of course, you can place your own trademarks or logos on versions of the
software to which you have made substantive modifications, because by modifying
the software, you have become the origin of that exact version. In that case,
you should not use our Marks.
However, you may use our Marks for the distribution of code (source or
executable) on the condition that any executable is built from an official TREK
source code release and that any modifications are limited to switching on or
off features already included in the software, translations into other
languages, and incorporating minor bug-fix patches. Use of our Marks on any
further modification is not permitted.
## Mobile wrappers, hosted instances, and forks
The following clarifications apply specifically to common ways TREK is
redistributed:
* **Self-hosted instances of unmodified TREK.** You may refer to your instance
as "a TREK instance" or "running TREK." You may not name the service itself
in a way that suggests it is the official TREK ("TREK Cloud," "TREK
Official," etc.).
* **Mobile wrappers (WebView shells, Capacitor apps, native apps) pointing at
TREK.** You may describe your app as "a mobile client for TREK" or "for use
with TREK." You may not publish it on app stores under the name "TREK" or a
confusingly similar name, and you may not use the TREK logo as the app icon
unless your wrapper distributes only an unmodified, official TREK instance
and you have obtained permission.
* **Forks of the TREK source code.** Forks that diverge from upstream must use
a different name. You may state that your fork is "based on TREK" or "a fork
of TREK," but the project name itself must be your own.
## Statements about your software's relation to TREK
You may use the word mark, but not TREK logos, to truthfully describe the
relationship between your software and ours. The word mark "TREK" should be
used after a verb or preposition that describes the relationship between your
software and ours. So you may say, for example, "Bob's app for TREK" but may
not say "Bob's TREK app." Some other examples that may work for you are:
* [Your software] uses TREK
* [Your software] is powered by TREK
* [Your software] runs on TREK
* [Your software] for use with TREK
* [Your software] for TREK
## Questions and permission requests
If you are not sure whether your intended use of the Marks is permitted under
this policy, or if you would like to request explicit permission for a use that
is not covered, please open an issue on the TREK GitHub repository or contact
the maintainers directly.
---
These guidelines are based on the
[Model Trademark Guidelines](http://www.modeltrademarkguidelines.org), used
under a
[Creative Commons Attribution 3.0 Unported license](https://creativecommons.org/licenses/by/3.0/deed.en_US).
+2 -2
View File
@@ -1,5 +1,5 @@
apiVersion: v2 apiVersion: v2
name: trek name: trek
version: 2.9.14 version: 3.0.9
description: Minimal Helm chart for TREK app description: Minimal Helm chart for TREK app
appVersion: "2.9.14" appVersion: "3.0.9"
+3
View File
@@ -22,6 +22,9 @@ data:
{{- if .Values.env.FORCE_HTTPS }} {{- if .Values.env.FORCE_HTTPS }}
FORCE_HTTPS: {{ .Values.env.FORCE_HTTPS | quote }} FORCE_HTTPS: {{ .Values.env.FORCE_HTTPS | quote }}
{{- end }} {{- end }}
{{- if .Values.env.HSTS_INCLUDE_SUBDOMAINS }}
HSTS_INCLUDE_SUBDOMAINS: {{ .Values.env.HSTS_INCLUDE_SUBDOMAINS | quote }}
{{- end }}
{{- if .Values.env.COOKIE_SECURE }} {{- if .Values.env.COOKIE_SECURE }}
COOKIE_SECURE: {{ .Values.env.COOKIE_SECURE | quote }} COOKIE_SECURE: {{ .Values.env.COOKIE_SECURE | quote }}
{{- end }} {{- end }}
+2
View File
@@ -30,6 +30,8 @@ env:
# Also used as the base URL for links in email notifications and other external links. # Also used as the base URL for links in email notifications and other external links.
# FORCE_HTTPS: "false" # FORCE_HTTPS: "false"
# Optional. When "true": HTTPS redirect, HSTS, CSP upgrade-insecure-requests, secure cookies. Only behind a TLS proxy. Requires TRUST_PROXY. # Optional. When "true": HTTPS redirect, HSTS, CSP upgrade-insecure-requests, secure cookies. Only behind a TLS proxy. Requires TRUST_PROXY.
# HSTS_INCLUDE_SUBDOMAINS: "false"
# When "true": adds includeSubDomains to the HSTS header. Only effective when HSTS is active. Leave "false" if sibling subdomains still run over plain HTTP.
# COOKIE_SECURE: "true" # COOKIE_SECURE: "true"
# Auto-derived (true in production or when FORCE_HTTPS=true). Set "false" to force cookies over plain HTTP. Not recommended for production. # Auto-derived (true in production or when FORCE_HTTPS=true). Set "false" to force cookies over plain HTTP. Not recommended for production.
# TRUST_PROXY: "1" # TRUST_PROXY: "1"
+5 -5
View File
@@ -1,12 +1,12 @@
{ {
"name": "trek-client", "name": "trek-client",
"version": "2.9.14", "version": "3.0.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trek-client", "name": "trek-client",
"version": "2.9.14", "version": "3.0.9",
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"axios": "^1.6.7", "axios": "^1.6.7",
@@ -8907,9 +8907,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.9", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "trek-client", "name": "trek-client",
"version": "2.9.14", "version": "3.0.9",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -634,7 +634,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
} }
const handleRenameCategory = async (oldName, newName) => { const handleRenameCategory = async (oldName, newName) => {
if (!newName.trim() || newName.trim() === oldName) return if (!newName.trim() || newName.trim() === oldName) return
const items = grouped[oldName] || [] const items = grouped.get(oldName) || []
for (const item of Array.from(items)) await updateBudgetItem(tripId, item.id, { category: newName.trim() }) for (const item of Array.from(items)) await updateBudgetItem(tripId, item.id, { category: newName.trim() })
} }
const handleAddCategory = () => { const handleAddCategory = () => {
+12 -5
View File
@@ -266,17 +266,22 @@ export default function DemoBanner(): React.ReactElement | null {
return ( return (
<div style={{ <div style={{
position: 'fixed', inset: 0, zIndex: 9999, position: 'fixed', inset: 0, zIndex: 99999,
background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)', background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 16, overflow: 'auto', paddingTop: 'max(16px, env(safe-area-inset-top))',
paddingBottom: 'max(16px, calc(env(safe-area-inset-bottom) + 80px))',
paddingLeft: 16, paddingRight: 16,
overflow: 'auto',
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif", fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
}} onClick={() => setDismissed(true)}> }} onClick={() => setDismissed(true)}>
<div style={{ <div style={{
background: 'white', borderRadius: 20, padding: '28px 24px 20px', background: 'white', borderRadius: 20, padding: '28px 24px 0',
maxWidth: 480, width: '100%', maxWidth: 480, width: '100%',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)', boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
maxHeight: '90vh', overflow: 'auto', maxHeight: 'min(90vh, calc(100dvh - 96px))',
overflow: 'auto',
display: 'flex', flexDirection: 'column',
}} onClick={(e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation()}> }} onClick={(e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation()}>
{/* Header */} {/* Header */}
@@ -367,8 +372,10 @@ export default function DemoBanner(): React.ReactElement | null {
{/* Footer */} {/* Footer */}
<div style={{ <div style={{
paddingTop: 14, borderTop: '1px solid #e5e7eb', padding: '14px 0 20px', borderTop: '1px solid #e5e7eb',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
position: 'sticky', bottom: 0, background: 'white',
marginTop: 'auto',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#9ca3af' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#9ca3af' }}>
<Github size={13} /> <Github size={13} />
@@ -78,6 +78,7 @@ const transportReservation = {
id: 400, id: 400,
title: 'Flight to Rome', title: 'Flight to Rome',
type: 'flight', type: 'flight',
day_id: 10,
reservation_time: '2025-06-01T14:30:00', reservation_time: '2025-06-01T14:30:00',
confirmation_number: 'ABC123', confirmation_number: 'ABC123',
metadata: JSON.stringify({ airline: 'Air Italia', flight_number: 'AI123', departure_airport: 'CDG', arrival_airport: 'FCO' }), metadata: JSON.stringify({ airline: 'Air Italia', flight_number: 'AI123', departure_airport: 'CDG', arrival_airport: 'FCO' }),
+47 -8
View File
@@ -140,23 +140,58 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
const totalCost = Object.values(assignments || {}) const totalCost = Object.values(assignments || {})
.flatMap(a => a).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0) .flatMap(a => a).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0)
// Span helpers for multi-day transport (mirrors DayPlanSidebar logic)
const pdfGetDayOrder = (d: Day) => d.day_number
const pdfGetSpanPhase = (r: any, dayId: number): 'single' | 'start' | 'middle' | 'end' => {
const startId = r.day_id
const endId = r.end_day_id ?? startId
if (!startId || startId === endId) return 'single'
if (dayId === startId) return 'start'
if (dayId === endId) return 'end'
return 'middle'
}
const pdfGetDisplayTime = (r: any, dayId: number): string | null => {
const phase = pdfGetSpanPhase(r, dayId)
if (phase === 'end') return r.reservation_end_time || null
if (phase === 'middle') return null
return r.reservation_time || null
}
const pdfGetSpanLabel = (r: any, phase: string): string | null => {
if (phase === 'single') return null
if (r.type === 'flight') return tr(`reservations.span.${phase === 'start' ? 'departure' : phase === 'end' ? 'arrival' : 'inTransit'}`)
if (r.type === 'car') return tr(`reservations.span.${phase === 'start' ? 'pickup' : phase === 'end' ? 'return' : 'active'}`)
return tr(`reservations.span.${phase === 'start' ? 'start' : phase === 'end' ? 'end' : 'ongoing'}`)
}
const pdfGetTransportForDay = (dayId: number) => (reservations || []).filter(r => {
if (r.type === 'hotel') return false
const startId = r.day_id
const endId = r.end_day_id ?? startId
if (startId == null) return false
if (endId !== startId) {
const startDay = sorted.find(d => d.id === startId)
const endDay = sorted.find(d => d.id === endId)
const thisDay = sorted.find(d => d.id === dayId)
if (!startDay || !endDay || !thisDay) return false
return pdfGetDayOrder(thisDay) >= pdfGetDayOrder(startDay) && pdfGetDayOrder(thisDay) <= pdfGetDayOrder(endDay)
}
return startId === dayId
})
// Build day HTML // Build day HTML
const daysHtml = sorted.map((day, di) => { const daysHtml = sorted.map((day, di) => {
const assigned = assignments[String(day.id)] || [] const assigned = assignments[String(day.id)] || []
const notes = (dayNotes || []).filter(n => n.day_id === day.id) const notes = (dayNotes || []).filter(n => n.day_id === day.id)
const cost = dayCost(assignments, day.id, loc) const cost = dayCost(assignments, day.id, loc)
// Reservations for this day (hotel rendered via accommodations block) // Reservations for this day (hotel rendered via accommodations block; car middle-phase rendered in sidebar header only)
const dayReservations = (reservations || []).filter(r => { const dayReservations = pdfGetTransportForDay(day.id)
if (!r.reservation_time || r.type === 'hotel') return false .filter(r => !(r.type === 'car' && pdfGetSpanPhase(r, day.id) === 'middle'))
return day.date && r.reservation_time.split('T')[0] === day.date
})
const merged = [] const merged = []
assigned.forEach(a => merged.push({ type: 'place', k: a.order_index ?? a.sort_order ?? 0, data: a })) assigned.forEach(a => merged.push({ type: 'place', k: a.order_index ?? a.sort_order ?? 0, data: a }))
notes.forEach(n => merged.push({ type: 'note', k: n.sort_order ?? 0, data: n })) notes.forEach(n => merged.push({ type: 'note', k: n.sort_order ?? 0, data: n }))
dayReservations.forEach(r => { dayReservations.forEach(r => {
const pos = r.day_plan_position ?? (merged.length > 0 ? Math.max(...merged.map(m => m.k)) + 0.5 : 0.5) const pos = r.day_positions?.[day.id] ?? r.day_positions?.[String(day.id)] ?? r.day_plan_position ?? (merged.length > 0 ? Math.max(...merged.map(m => m.k)) + 0.5 : 0.5)
merged.push({ type: 'reservation', k: pos, data: r }) merged.push({ type: 'reservation', k: pos, data: r })
}) })
merged.sort((a, b) => a.k - b.k) merged.sort((a, b) => a.k - b.k)
@@ -177,13 +212,17 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
else if (r.type === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ') else if (r.type === 'event') subtitle = [meta.venue].filter(Boolean).join(' · ')
else if (r.type === 'tour') subtitle = [meta.operator].filter(Boolean).join(' · ') else if (r.type === 'tour') subtitle = [meta.operator].filter(Boolean).join(' · ')
const locationLine = r.location || meta.location || '' const locationLine = r.location || meta.location || ''
const time = r.reservation_time?.includes('T') ? r.reservation_time.split('T')[1]?.substring(0, 5) : '' const phase = pdfGetSpanPhase(r, day.id)
const spanLabel = pdfGetSpanLabel(r, phase)
const displayTime = pdfGetDisplayTime(r, day.id)
const time = displayTime?.includes('T') ? displayTime.split('T')[1]?.substring(0, 5) : ''
const titleHtml = `${spanLabel ? escHtml(spanLabel) + ': ' : ''}${escHtml(r.title)}`
return ` return `
<div class="note-card" style="border-left: 3px solid ${color};"> <div class="note-card" style="border-left: 3px solid ${color};">
<div class="note-line" style="background: ${color};"></div> <div class="note-line" style="background: ${color};"></div>
<span class="note-icon">${icon}</span> <span class="note-icon">${icon}</span>
<div class="note-body"> <div class="note-body">
<div class="note-text" style="font-weight: 600;">${escHtml(r.title)}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div> <div class="note-text" style="font-weight: 600;">${titleHtml}${time ? ` <span style="color:#6b7280;font-weight:400;font-size:10px;">${time}</span>` : ''}</div>
${subtitle ? `<div class="note-time">${escHtml(subtitle)}</div>` : ''} ${subtitle ? `<div class="note-time">${escHtml(subtitle)}</div>` : ''}
${locationLine ? `<div class="note-time">${escHtml(locationLine)}</div>` : ''} ${locationLine ? `<div class="note-time">${escHtml(locationLine)}</div>` : ''}
${r.confirmation_number ? `<div class="note-time" style="font-size:9px;">Code: ${escHtml(r.confirmation_number)}</div>` : ''} ${r.confirmation_number ? `<div class="note-time" style="font-size:9px;">Code: ${escHtml(r.confirmation_number)}</div>` : ''}
@@ -66,7 +66,11 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit' const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
const is12h = useSettingsStore(s => s.settings.time_format) === '12h' const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes) const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes)
const fmtTime = (v) => formatTime12(v, is12h) const fmtTime = (v) => {
if (!v) return v
if (v.includes('T')) return new Date(v).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: is12h })
return formatTime12(v, is12h)
}
const unit = isFahrenheit ? '°F' : '°C' const unit = isFahrenheit ? '°F' : '°C'
const collapsed = collapsedProp const collapsed = collapsedProp
const toggleCollapse = () => onToggleCollapse?.() const toggleCollapse = () => onToggleCollapse?.()
@@ -1576,7 +1576,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
{res.reservation_time?.includes('T') && ( {res.reservation_time?.includes('T') && (
<span style={{ fontWeight: 400 }}> <span style={{ fontWeight: 400 }}>
{new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })} {new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}
{res.reservation_end_time && ` ${res.reservation_end_time}`} {res.reservation_end_time && ` ${(() => {
const endStr = res.reservation_end_time.includes('T') ? res.reservation_end_time : (res.reservation_time.split('T')[0] + 'T' + res.reservation_end_time)
return new Date(endStr).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })
})()}`}
</span> </span>
)} )}
{(() => { {(() => {
@@ -182,6 +182,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
let combinedEndTime = form.reservation_end_time let combinedEndTime = form.reservation_end_time
if (form.end_date) { if (form.end_date) {
combinedEndTime = form.reservation_end_time ? `${form.end_date}T${form.reservation_end_time}` : form.end_date combinedEndTime = form.reservation_end_time ? `${form.end_date}T${form.reservation_end_time}` : form.end_date
} else if (form.reservation_end_time && form.reservation_time) {
combinedEndTime = `${form.reservation_time.split('T')[0]}T${form.reservation_end_time}`
} }
if (isBudgetEnabled) { if (isBudgetEnabled) {
if (form.price) metadata.price = form.price if (form.price) metadata.price = form.price
@@ -236,7 +236,16 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
<div style={fieldLabelStyle}>{t('reservations.date')}</div> <div style={fieldLabelStyle}>{t('reservations.date')}</div>
<div style={{ ...fieldValueStyle, textAlign: 'center' }}> <div style={{ ...fieldValueStyle, textAlign: 'center' }}>
{fmtDate(r.reservation_time)} {fmtDate(r.reservation_time)}
{r.reservation_end_time && (r.reservation_end_time.includes('T') ? r.reservation_end_time.split('T')[0] : r.reservation_end_time) !== r.reservation_time.split('T')[0] && ( {(() => {
const endDatePart = r.reservation_end_time
? r.reservation_end_time.includes('T')
? r.reservation_end_time.split('T')[0]
: /^\d{4}-\d{2}-\d{2}$/.test(r.reservation_end_time)
? r.reservation_end_time
: null
: null
return endDatePart && endDatePart !== r.reservation_time.split('T')[0]
})() && (
<> {fmtDate(r.reservation_end_time)}</> <> {fmtDate(r.reservation_end_time)}</>
)} )}
</div> </div>
@@ -41,7 +41,7 @@ export default function ConfirmDialog({
return ( return (
<div <div
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter" className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)' }} style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)', paddingBottom: 'var(--bottom-nav-h)' }}
onClick={onClose} onClick={onClose}
> >
<div <div
@@ -42,7 +42,7 @@ export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }
return ( return (
<div <div
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter" className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)' }} style={{ backgroundColor: 'rgba(15, 23, 42, 0.5)', paddingBottom: 'var(--bottom-nav-h)' }}
onClick={onClose} onClick={onClose}
> >
<div <div
+5 -2
View File
@@ -343,7 +343,10 @@ export default function TripPlannerPage(): React.ReactElement | null {
}, [tripId]) }, [tripId])
useEffect(() => { useEffect(() => {
if (tripId) tripActions.loadReservations(tripId) if (tripId) {
tripActions.loadReservations(tripId)
tripActions.loadBudgetItems?.(tripId)
}
}, [tripId]) }, [tripId])
useTripWebSocket(tripId) useTripWebSocket(tripId)
@@ -1106,7 +1109,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
</div> </div>
<div style={{ flex: 1, overflow: 'auto' }}> <div style={{ flex: 1, overflow: 'auto' }}>
{mobileSidebarOpen === 'left' {mobileSidebarOpen === 'left'
? <DayPlanSidebar tripId={tripId} trip={trip} days={days} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} selectedAssignmentId={selectedAssignmentId} onSelectDay={(id) => { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId); setMobileSidebarOpen(null) }} onReorder={handleReorder} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute(r.coordinates); setRouteInfo({ distance: r.distanceText, duration: r.durationText }) } }} reservations={reservations} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null); setMobileSidebarOpen(null) }} accommodations={tripAccommodations} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} /> ? <DayPlanSidebar tripId={tripId} trip={trip} days={days} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} selectedAssignmentId={selectedAssignmentId} onSelectDay={(id) => { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId); setMobileSidebarOpen(null) }} onReorder={handleReorder} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute(r.coordinates); setRouteInfo({ distance: r.distanceText, duration: r.durationText }) } }} reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null); setMobileSidebarOpen(null) }} accommodations={tripAccommodations} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} />
: <PlacesSidebar tripId={tripId} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} onPlaceClick={(placeId) => { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { setEditingPlace(place); setEditingAssignmentId(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} onBulkDeletePlaces={(ids) => setDeletePlaceIds(ids)} onBulkDeleteConfirm={(ids) => confirmDeletePlaces(ids)} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} onPlacesFilterChange={setMapPlacesFilter} pushUndo={pushUndo} /> : <PlacesSidebar tripId={tripId} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} onPlaceClick={(placeId) => { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { setEditingPlace(place); setEditingAssignmentId(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} onBulkDeletePlaces={(ids) => setDeletePlaceIds(ids)} onBulkDeleteConfirm={(ids) => confirmDeletePlaces(ids)} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} onPlacesFilterChange={setMapPlacesFilter} pushUndo={pushUndo} />
} }
</div> </div>
+31
View File
@@ -355,6 +355,37 @@ describe('journeyStore', () => {
expect(useJourneyStore.getState().loading).toBe(false); expect(useJourneyStore.getState().loading).toBe(false);
}); });
// ── reorderEntries ───────────────────────────────────────────────────────
it('FE-STORE-JOURNEY-018: reorderEntries reorders by sort_order not entry_time', async () => {
const a = buildEntry({ id: 201, entry_date: '2026-04-01', entry_time: '09:00', sort_order: 0 });
const b = buildEntry({ id: 202, entry_date: '2026-04-01', entry_time: '11:00', sort_order: 1 });
const c = buildEntry({ id: 203, entry_date: '2026-04-01', entry_time: '14:00', sort_order: 2 });
const detail = buildJourneyDetail({ id: 55, entries: [a, b, c] });
useJourneyStore.setState({ current: detail });
server.use(
http.put('/api/journeys/55/entries/reorder', () => HttpResponse.json({ success: true }))
);
await useJourneyStore.getState().reorderEntries(55, [202, 201, 203]);
const ids = useJourneyStore.getState().current?.entries.map(e => e.id);
expect(ids).toEqual([202, 201, 203]);
});
it('FE-STORE-JOURNEY-019: reorderEntries rolls back on API failure', async () => {
const a = buildEntry({ id: 211, entry_date: '2026-04-01', sort_order: 0 });
const b = buildEntry({ id: 212, entry_date: '2026-04-01', sort_order: 1 });
const detail = buildJourneyDetail({ id: 56, entries: [a, b] });
useJourneyStore.setState({ current: detail });
server.use(
http.put('/api/journeys/56/entries/reorder', () => HttpResponse.json({}, { status: 403 }))
);
await expect(useJourneyStore.getState().reorderEntries(56, [212, 211])).rejects.toBeTruthy();
const ids = useJourneyStore.getState().current?.entries.map(e => e.id);
expect(ids).toEqual([211, 212]);
});
// ── clear ──────────────────────────────────────────────────────────────── // ── clear ────────────────────────────────────────────────────────────────
it('FE-STORE-JOURNEY-015: clear resets state', () => { it('FE-STORE-JOURNEY-015: clear resets state', () => {
+2 -4
View File
@@ -223,10 +223,8 @@ export const useJourneyStore = create<JourneyState>((set, get) => ({
) )
entries.sort((a, b) => { entries.sort((a, b) => {
if (a.entry_date !== b.entry_date) return a.entry_date.localeCompare(b.entry_date) if (a.entry_date !== b.entry_date) return a.entry_date.localeCompare(b.entry_date)
const atime = a.entry_time || '' if (a.sort_order !== b.sort_order) return (a.sort_order || 0) - (b.sort_order || 0)
const btime = b.entry_time || '' return a.id - b.id
if (atime !== btime) return atime.localeCompare(btime)
return (a.sort_order || 0) - (b.sort_order || 0)
}) })
return { current: { ...s.current, entries } } return { current: { ...s.current, entries } }
}) })
+27 -6
View File
@@ -32,6 +32,13 @@ function triggerAnchorDownload(blobUrl: string, filename?: string): void {
setTimeout(() => { URL.revokeObjectURL(blobUrl); a.remove() }, 100) setTimeout(() => { URL.revokeObjectURL(blobUrl); a.remove() }, 100)
} }
// navigator.standalone is true only on iOS when running as an
// add-to-home-screen PWA. In that context, target="_blank" hands off to
// Safari, which cannot access blob URLs sandboxed to the WebView.
function isIosStandalone(): boolean {
return (navigator as any).standalone === true
}
/** /**
* Fetches a protected file using cookie auth (credentials: include) and * Fetches a protected file using cookie auth (credentials: include) and
* triggers a browser download. Works inside PWA standalone mode because the * triggers a browser download. Works inside PWA standalone mode because the
@@ -56,7 +63,13 @@ export async function downloadFile(url: string, filename?: string): Promise<void
* (including text/html and image/svg+xml which can execute script) are forced * (including text/html and image/svg+xml which can execute script) are forced
* to download so that an uploaded file cannot run code in the TREK origin. * to download so that an uploaded file cannot run code in the TREK origin.
* *
* Falls back to a download trigger if the popup is blocked. * Uses a synthetic <a target="_blank" rel="noopener noreferrer"> click rather
* than window.open(). window.open() called with the "noreferrer"/"noopener"
* window feature returns null per spec, which previously made the popup-block
* fallback trigger a download in the *current* tab on top of the new-tab open
* i.e. the file opened twice. The anchor approach avoids that ambiguity:
* the new tab is opened by the browser's normal link-handling path, and no
* spurious in-page download is triggered.
*/ */
export async function openFile(url: string, filename?: string): Promise<void> { export async function openFile(url: string, filename?: string): Promise<void> {
assertRelativeUrl(url) assertRelativeUrl(url)
@@ -71,11 +84,19 @@ export async function openFile(url: string, filename?: string): Promise<void> {
return return
} }
const win = window.open(blobUrl, '_blank', 'noreferrer') // iOS PWA: target="_blank" would open Safari, which can't access the blob
if (win) { if (isIosStandalone()) {
setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000)
} else {
// Popup blocked — fall back to download
triggerAnchorDownload(blobUrl, filename) triggerAnchorDownload(blobUrl, filename)
return
} }
const a = document.createElement('a')
a.href = blobUrl
a.target = '_blank'
a.rel = 'noopener noreferrer'
document.body.appendChild(a)
a.click()
// Keep the blob URL alive long enough for the new tab to load it, then
// clean up the DOM node and revoke the URL.
setTimeout(() => { URL.revokeObjectURL(blobUrl); a.remove() }, 30_000)
} }
+53 -16
View File
@@ -74,32 +74,42 @@ describe('downloadFile', () => {
}) })
describe('openFile', () => { describe('openFile', () => {
it('fetches with credentials:include and opens blob URL in new tab', async () => { it('fetches with credentials:include and opens blob URL via target=_blank anchor', async () => {
vi.stubGlobal('fetch', makeFetchMock(200)) vi.stubGlobal('fetch', makeFetchMock(200))
const mockWin = { closed: false } const openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
const openSpy = vi.spyOn(window, 'open').mockReturnValue(mockWin as Window) const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
await openFile('/uploads/files/doc.pdf') await openFile('/uploads/files/doc.pdf')
expect(window.fetch).toHaveBeenCalledWith('/uploads/files/doc.pdf', { credentials: 'include' }) expect(window.fetch).toHaveBeenCalledWith('/uploads/files/doc.pdf', { credentials: 'include' })
expect(URL.createObjectURL).toHaveBeenCalled() expect(URL.createObjectURL).toHaveBeenCalled()
expect(openSpy).toHaveBeenCalledWith('blob:mock-url', '_blank', 'noreferrer') // Must NOT call window.open — that path returns null when noreferrer is
// set, which previously caused the file to also open in the current tab.
expect(openSpy).not.toHaveBeenCalled()
expect(clickSpy).toHaveBeenCalledTimes(1)
// The anchor used to open the new tab must be target=_blank, must NOT
// carry a `download` attribute (otherwise it would download in-page
// instead of opening), and must use rel=noopener noreferrer.
const appendCalls = (document.body.appendChild as ReturnType<typeof vi.fn>).mock.calls
const anchor = appendCalls[0]?.[0] as HTMLAnchorElement
expect(anchor.target).toBe('_blank')
expect(anchor.rel).toBe('noopener noreferrer')
expect(anchor.hasAttribute('download')).toBe(false)
// Revoke happens after 30s timeout // Revoke happens after 30s timeout
vi.runAllTimers() vi.runAllTimers()
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url') expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
}) })
it('falls back to anchor download when popup is blocked', async () => { it('does not trigger a second in-page action for safe inline types (regression: no double-open)', async () => {
vi.stubGlobal('fetch', makeFetchMock(200)) vi.stubGlobal('fetch', makeFetchMock(200))
vi.spyOn(window, 'open').mockReturnValue(null)
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
await openFile('/uploads/files/doc.pdf') await openFile('/uploads/files/doc.pdf', 'doc.pdf')
expect(clickSpy).toHaveBeenCalled() // Exactly ONE anchor click — opening the new tab. No fallback download.
vi.runAllTimers() expect(clickSpy).toHaveBeenCalledTimes(1)
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
}) })
it('throws on 401 response', async () => { it('throws on 401 response', async () => {
@@ -108,28 +118,55 @@ describe('openFile', () => {
expect(URL.createObjectURL).not.toHaveBeenCalled() expect(URL.createObjectURL).not.toHaveBeenCalled()
}) })
it('forces download for unsafe MIME types (HTML, SVG) instead of opening inline', async () => { it('forces download for unsafe MIME types (HTML) instead of opening inline', async () => {
const htmlBlob = new Blob(['<script>alert(1)</script>'], { type: 'text/html' }) const htmlBlob = new Blob(['<script>alert(1)</script>'], { type: 'text/html' })
vi.stubGlobal('fetch', makeFetchMock(200, htmlBlob)) vi.stubGlobal('fetch', makeFetchMock(200, htmlBlob))
const openSpy = vi.spyOn(window, 'open').mockReturnValue({} as Window) const openSpy = vi.spyOn(window, 'open').mockReturnValue({} as Window)
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
await openFile('/uploads/files/malicious.html') await openFile('/uploads/files/malicious.html', 'malicious.html')
// Must NOT open inline — download anchor clicked instead // Must NOT open inline — download anchor clicked instead
expect(openSpy).not.toHaveBeenCalled() expect(openSpy).not.toHaveBeenCalled()
expect(clickSpy).toHaveBeenCalled() expect(clickSpy).toHaveBeenCalledTimes(1)
const appendCalls = (document.body.appendChild as ReturnType<typeof vi.fn>).mock.calls
const anchor = appendCalls[0]?.[0] as HTMLAnchorElement
expect(anchor.download).toBe('malicious.html')
}) })
it('forces download for SVG MIME type', async () => { it('forces download for SVG MIME type', async () => {
const svgBlob = new Blob(['<svg><script>alert(1)</script></svg>'], { type: 'image/svg+xml' }) const svgBlob = new Blob(['<svg><script>alert(1)</script></svg>'], { type: 'image/svg+xml' })
vi.stubGlobal('fetch', makeFetchMock(200, svgBlob)) vi.stubGlobal('fetch', makeFetchMock(200, svgBlob))
vi.spyOn(window, 'open').mockReturnValue({} as Window) const openSpy = vi.spyOn(window, 'open').mockReturnValue({} as Window)
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
await openFile('/uploads/files/malicious.svg') await openFile('/uploads/files/malicious.svg')
expect(window.open).not.toHaveBeenCalled() expect(openSpy).not.toHaveBeenCalled()
expect(clickSpy).toHaveBeenCalled() expect(clickSpy).toHaveBeenCalledTimes(1)
})
it('falls back to download in iOS PWA standalone mode (blob URL inaccessible to Safari)', async () => {
vi.stubGlobal('fetch', makeFetchMock(200))
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
// Simulate iOS PWA (Add-to-Home-Screen) context
Object.defineProperty(navigator, 'standalone', { configurable: true, value: true })
try {
await openFile('/uploads/files/doc.pdf', 'doc.pdf')
// Single anchor click — and it must be a DOWNLOAD anchor (no target=_blank),
// because target="_blank" in iOS PWA would hand off to Safari which cannot
// read the in-WebView blob URL.
expect(clickSpy).toHaveBeenCalledTimes(1)
const appendCalls = (document.body.appendChild as ReturnType<typeof vi.fn>).mock.calls
const anchor = appendCalls[0]?.[0] as HTMLAnchorElement
expect(anchor.target).toBe('')
expect(anchor.download).toBe('doc.pdf')
} finally {
// Clean up the non-standard iOS-only property we forced above.
delete (navigator as any).standalone
}
}) })
}) })
+1
View File
@@ -24,6 +24,7 @@ services:
# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar # - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links
# - FORCE_HTTPS=true # Optional. Enables HTTPS redirect, HSTS, CSP upgrade-insecure-requests, and secure cookies behind a TLS proxy # - FORCE_HTTPS=true # Optional. Enables HTTPS redirect, HSTS, CSP upgrade-insecure-requests, and secure cookies behind a TLS proxy
# - HSTS_INCLUDE_SUBDOMAINS=false # When true: adds includeSubDomains to the HSTS header. Only effective when HSTS is active. Leave false if sibling subdomains still run over plain HTTP.
# - COOKIE_SECURE=false # Escape hatch: force session cookies over plain HTTP even in production. Not recommended. # - COOKIE_SECURE=false # Escape hatch: force session cookies over plain HTTP even in production. Not recommended.
# - TRUST_PROXY=1 # Trusted proxy count for X-Forwarded-For / X-Forwarded-Proto. Required for FORCE_HTTPS to work. # - TRUST_PROXY=1 # Trusted proxy count for X-Forwarded-For / X-Forwarded-Proto. Required for FORCE_HTTPS to work.
# - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless. # - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless.
+56
View File
@@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
+98
View File
@@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
+35
View File
@@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+11956
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
{
"name": "server-nest-2",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@better-auth/oauth-provider": "^1.6.9",
"@better-auth/passkey": "^1.6.9",
"@hedystia/better-auth-typeorm": "^0.8.2",
"@nestjs/common": "^11.1.19",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.1.19",
"@nestjs/platform-express": "^11.1.19",
"@nestjs/platform-socket.io": "^11.1.19",
"@nestjs/throttler": "^6.5.0",
"@nestjs/typeorm": "^11.0.1",
"@nestjs/websockets": "^11.1.19",
"@thallesp/nestjs-better-auth": "^2.6.0",
"better-auth": "^1.6.9",
"better-sqlite3": "^12.9.0",
"csrf-csrf": "^4.0.3",
"helmet": "^8.1.0",
"mysql2": "^3.22.2",
"pg": "^8.20.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.0",
"@types/supertest": "^7.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^17.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
+46
View File
@@ -0,0 +1,46 @@
import authConfig from './config/auth.config.js';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module.js';
type SupportedDbType = 'mysql' | 'mariadb' | 'postgres' | 'sqlite';
function resolveDriver(type: SupportedDbType) {
switch (type) {
case 'mysql':
case 'mariadb':
return require('mysql2');
case 'postgres':
return require('pg');
case 'sqlite':
return require('better-sqlite3');
}
}
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [authConfig] }),
AuthModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => {
const type = config.get<SupportedDbType>('DB_TYPE', 'sqlite');
return {
type,
driver: resolveDriver(type),
host: config.get<string>('DB_HOST', 'localhost'),
port: config.get<number>('DB_PORT', 5432),
username: config.get<string>('DB_USER', 'usr'),
password: config.get<string>('DB_PASS', 'pwd'),
database: config.get<string>('DB_NAME', 'data/travel.db'),
autoLoadEntities: true,
synchronize: config.get<string>('NODE_ENV') !== 'production',
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}
+20
View File
@@ -0,0 +1,20 @@
import { betterAuth } from 'better-auth';
import { typeormAdapter } from '@hedystia/better-auth-typeorm';
import { DataSource } from 'typeorm';
import { AuthConfig } from '../config/auth.config.js';
export function createAuth(dataSource: DataSource, cfg: AuthConfig) {
return betterAuth({
database: typeormAdapter(dataSource, { debugLogs: cfg.debugLogs }),
secret: cfg.secret,
baseURL: cfg.baseURL,
basePath: '/api/auth',
trustedOrigins: cfg.frontendUrl ? [cfg.frontendUrl] : [],
advanced: {
cookies: { session_token: { name: 'trek_session' } },
useSecureCookies: cfg.cookieSecure,
},
emailAndPassword: { enabled: true },
plugins: [],
});
}
+39
View File
@@ -0,0 +1,39 @@
import { betterAuth } from 'better-auth';
import { typeormAdapter } from '@hedystia/better-auth-typeorm';
import { magicLink } from 'better-auth/plugins/magic-link';
import { genericOAuth } from 'better-auth/plugins/generic-oauth';
import { jwt } from 'better-auth/plugins/jwt';
import { oauthProvider } from '@better-auth/oauth-provider';
import { passkey } from '@better-auth/passkey';
import { DataSource } from 'typeorm';
// Used only by `npx @better-auth/cli generate`.
// Not imported at runtime — auth.factory.ts uses the DI DataSource.
const dataSource = new DataSource({
type: 'better-sqlite3',
database: ':memory:',
});
export const auth = betterAuth({
baseURL: 'http://localhost:3000',
database: typeormAdapter(dataSource, {
entitiesDir: './src/models/entities/auth',
migrationsDir: './src/database/migrations',
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendVerificationEmail: async () => {},
sendResetPassword: async () => {},
},
emailVerification: {
sendVerificationEmail: async () => {},
},
plugins: [
jwt(),
magicLink({ sendMagicLink: async () => {} }),
genericOAuth({ config: [] }),
oauthProvider({ loginPage: '/login' }),
passkey(),
],
});
+43
View File
@@ -0,0 +1,43 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule as BetterAuthNestModule } from '@thallesp/nestjs-better-auth';
import { DataSource } from 'typeorm';
import { createAuth } from './auth.factory.js';
import { AuthConfig } from '../config/auth.config.js';
import { User } from '../models/entities/auth/User.js';
import { Account } from '../models/entities/auth/Account.js';
import { Session } from '../models/entities/auth/Session.js';
import { Verification } from '../models/entities/auth/Verification.js';
import { Passkey } from '../models/entities/auth/Passkey.js';
import { Jwks } from '../models/entities/auth/Jwks.js';
import { OauthClient } from '../models/entities/auth/OauthClient.js';
import { OauthAccessToken } from '../models/entities/auth/OauthAccessToken.js';
import { OauthRefreshToken } from '../models/entities/auth/OauthRefreshToken.js';
import { OauthConsent } from '../models/entities/auth/OauthConsent.js';
@Module({
imports: [
TypeOrmModule.forFeature([
User,
Account,
Session,
Verification,
Passkey,
Jwks,
OauthClient,
OauthAccessToken,
OauthRefreshToken,
OauthConsent,
]),
BetterAuthNestModule.forRootAsync({
imports: [ConfigModule],
inject: [DataSource, ConfigService],
useFactory: (ds: DataSource, config: ConfigService) => ({
auth: createAuth(ds, config.get<AuthConfig>('auth')!),
}),
}),
],
exports: [BetterAuthNestModule],
})
export class AuthModule {}
+26
View File
@@ -0,0 +1,26 @@
import { registerAs } from '@nestjs/config';
import { boolean } from 'better-auth';
export interface AuthConfig {
secret: string;
baseURL: string;
frontendUrl: string | undefined;
cookieSecure: boolean;
debugLogs: boolean;
}
export default registerAs(
'auth',
(): AuthConfig => ({
secret: process.env.BETTER_AUTH_SECRET ?? 'changeme',
baseURL: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000',
frontendUrl: process.env.BASE_URL,
cookieSecure:
process.env.COOKIE_SECURE === 'true' &&
process.env.NODE_ENV === 'production' &&
process.env.BASE_URL?.startsWith('https') === true,
debugLogs:
process.env.BETTER_AUTH_DEBUG_LOGS === 'true' ||
process.env.NODE_ENV === 'development',
}),
);
@@ -0,0 +1,103 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateAccount1777216318138 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'account',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accountId',
type: 'text',
},
{
name: 'providerId',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
},
{
name: 'idToken',
type: 'text',
isNullable: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scope',
type: 'text',
isNullable: true,
},
{
name: 'password',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
],
}),
);
await queryRunner.createIndex(
'account',
new TableIndex({
name: 'account_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'account',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('account');
}
}
@@ -0,0 +1,79 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateSession1777216318138 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'session',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
{
name: 'ipAddress',
type: 'text',
isNullable: true,
},
{
name: 'userAgent',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
],
}),
);
await queryRunner.createIndex(
'session',
new TableIndex({
name: 'session_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'session',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('session');
}
}
@@ -0,0 +1,58 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateUser1777216318138 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
isUnique: true,
},
{
name: 'emailVerified',
type: 'boolean',
default: false,
},
{
name: 'image',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
@@ -0,0 +1,59 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateVerification1777216318138 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'verification',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'identifier',
type: 'text',
},
{
name: 'value',
type: 'text',
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
await queryRunner.createIndex(
'verification',
new TableIndex({
name: 'verification_identifier_idx',
columnNames: ['identifier'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('verification');
}
}
@@ -0,0 +1,103 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateAccount1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'account',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accountId',
type: 'text',
},
{
name: 'providerId',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
},
{
name: 'idToken',
type: 'text',
isNullable: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scope',
type: 'text',
isNullable: true,
},
{
name: 'password',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
],
}),
);
await queryRunner.createIndex(
'account',
new TableIndex({
name: 'account_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'account',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('account');
}
}
@@ -0,0 +1,112 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthAccessToken1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthAccessToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'clientId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'oauthAccessToken',
new TableIndex({
name: 'oauthAccessToken_clientId_idx',
columnNames: ['clientId'],
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthApplication',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'oauthAccessToken',
new TableIndex({
name: 'oauthAccessToken_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthAccessToken');
}
}
@@ -0,0 +1,104 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthApplication1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthApplication',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'icon',
type: 'text',
isNullable: true,
},
{
name: 'metadata',
type: 'text',
isNullable: true,
},
{
name: 'clientId',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'clientSecret',
type: 'text',
isNullable: true,
},
{
name: 'redirectUrls',
type: 'text',
isNullable: true,
},
{
name: 'type',
type: 'text',
isNullable: true,
},
{
name: 'disabled',
type: 'boolean',
isNullable: true,
default: false,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'oauthApplication',
new TableIndex({
name: 'oauthApplication_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'oauthApplication',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthApplication');
}
}
@@ -0,0 +1,95 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthConsent1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthConsent',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
{
name: 'consentGiven',
type: 'boolean',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'oauthConsent',
new TableIndex({
name: 'oauthConsent_clientId_idx',
columnNames: ['clientId'],
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthApplication',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'oauthConsent',
new TableIndex({
name: 'oauthConsent_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthConsent');
}
}
@@ -0,0 +1,99 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreatePasskey1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'passkey',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'credentialID',
type: 'text',
},
{
name: 'counter',
type: 'integer',
},
{
name: 'deviceType',
type: 'text',
},
{
name: 'backedUp',
type: 'boolean',
},
{
name: 'transports',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'aaguid',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'passkey',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_credentialID_idx',
columnNames: ['credentialID'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('passkey');
}
}
@@ -0,0 +1,79 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateSession1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'session',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
{
name: 'ipAddress',
type: 'text',
isNullable: true,
},
{
name: 'userAgent',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
],
}),
);
await queryRunner.createIndex(
'session',
new TableIndex({
name: 'session_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'session',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('session');
}
}
@@ -0,0 +1,58 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateUser1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
isUnique: true,
},
{
name: 'emailVerified',
type: 'boolean',
default: false,
},
{
name: 'image',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
@@ -0,0 +1,59 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateVerification1777217712285 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'verification',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'identifier',
type: 'text',
},
{
name: 'value',
type: 'text',
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
await queryRunner.createIndex(
'verification',
new TableIndex({
name: 'verification_identifier_idx',
columnNames: ['identifier'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('verification');
}
}
@@ -0,0 +1,103 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateAccount1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'account',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accountId',
type: 'text',
},
{
name: 'providerId',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
},
{
name: 'idToken',
type: 'text',
isNullable: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scope',
type: 'text',
isNullable: true,
},
{
name: 'password',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
],
}),
);
await queryRunner.createIndex(
'account',
new TableIndex({
name: 'account_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'account',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('account');
}
}
@@ -0,0 +1,113 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthAccessToken1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthAccessToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'refreshId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['refreshId'],
referencedTableName: 'oauthRefreshToken',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthAccessToken');
}
}
@@ -0,0 +1,184 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthClient1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthClient',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
isUnique: true,
},
{
name: 'clientSecret',
type: 'text',
isNullable: true,
},
{
name: 'disabled',
type: 'boolean',
isNullable: true,
default: false,
},
{
name: 'skipConsent',
type: 'boolean',
isNullable: true,
},
{
name: 'enableEndSession',
type: 'boolean',
isNullable: true,
},
{
name: 'subjectType',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'uri',
type: 'text',
isNullable: true,
},
{
name: 'icon',
type: 'text',
isNullable: true,
},
{
name: 'contacts',
type: 'text',
isNullable: true,
},
{
name: 'tos',
type: 'text',
isNullable: true,
},
{
name: 'policy',
type: 'text',
isNullable: true,
},
{
name: 'softwareId',
type: 'text',
isNullable: true,
},
{
name: 'softwareVersion',
type: 'text',
isNullable: true,
},
{
name: 'softwareStatement',
type: 'text',
isNullable: true,
},
{
name: 'redirectUris',
type: 'text',
},
{
name: 'postLogoutRedirectUris',
type: 'text',
isNullable: true,
},
{
name: 'tokenEndpointAuthMethod',
type: 'text',
isNullable: true,
},
{
name: 'grantTypes',
type: 'text',
isNullable: true,
},
{
name: 'responseTypes',
type: 'text',
isNullable: true,
},
{
name: 'public',
type: 'boolean',
isNullable: true,
},
{
name: 'type',
type: 'text',
isNullable: true,
},
{
name: 'requirePKCE',
type: 'boolean',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'metadata',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthClient',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthClient');
}
}
@@ -0,0 +1,77 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthConsent1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthConsent',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthConsent');
}
}
@@ -0,0 +1,106 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthRefreshToken1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthRefreshToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'revoked',
type: 'datetime',
isNullable: true,
},
{
name: 'authTime',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthRefreshToken');
}
}
@@ -0,0 +1,99 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreatePasskey1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'passkey',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'credentialID',
type: 'text',
},
{
name: 'counter',
type: 'integer',
},
{
name: 'deviceType',
type: 'text',
},
{
name: 'backedUp',
type: 'boolean',
},
{
name: 'transports',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'aaguid',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'passkey',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_credentialID_idx',
columnNames: ['credentialID'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('passkey');
}
}
@@ -0,0 +1,79 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateSession1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'session',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
{
name: 'ipAddress',
type: 'text',
isNullable: true,
},
{
name: 'userAgent',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
],
}),
);
await queryRunner.createIndex(
'session',
new TableIndex({
name: 'session_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'session',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('session');
}
}
@@ -0,0 +1,58 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateUser1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
isUnique: true,
},
{
name: 'emailVerified',
type: 'boolean',
default: false,
},
{
name: 'image',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
@@ -0,0 +1,59 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateVerification1777217820713 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'verification',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'identifier',
type: 'text',
},
{
name: 'value',
type: 'text',
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
await queryRunner.createIndex(
'verification',
new TableIndex({
name: 'verification_identifier_idx',
columnNames: ['identifier'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('verification');
}
}
@@ -0,0 +1,103 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateAccount1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'account',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accountId',
type: 'text',
},
{
name: 'providerId',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
},
{
name: 'idToken',
type: 'text',
isNullable: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scope',
type: 'text',
isNullable: true,
},
{
name: 'password',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
],
}),
);
await queryRunner.createIndex(
'account',
new TableIndex({
name: 'account_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'account',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('account');
}
}
@@ -0,0 +1,46 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateJwks1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'jwks',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'privateKey',
type: 'text',
},
{
name: 'createdAt',
type: 'datetime',
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('jwks');
}
}
@@ -0,0 +1,113 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthAccessToken1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthAccessToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'refreshId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['refreshId'],
referencedTableName: 'oauthRefreshToken',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthAccessToken');
}
}
@@ -0,0 +1,184 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthClient1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthClient',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
isUnique: true,
},
{
name: 'clientSecret',
type: 'text',
isNullable: true,
},
{
name: 'disabled',
type: 'boolean',
isNullable: true,
default: false,
},
{
name: 'skipConsent',
type: 'boolean',
isNullable: true,
},
{
name: 'enableEndSession',
type: 'boolean',
isNullable: true,
},
{
name: 'subjectType',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'uri',
type: 'text',
isNullable: true,
},
{
name: 'icon',
type: 'text',
isNullable: true,
},
{
name: 'contacts',
type: 'text',
isNullable: true,
},
{
name: 'tos',
type: 'text',
isNullable: true,
},
{
name: 'policy',
type: 'text',
isNullable: true,
},
{
name: 'softwareId',
type: 'text',
isNullable: true,
},
{
name: 'softwareVersion',
type: 'text',
isNullable: true,
},
{
name: 'softwareStatement',
type: 'text',
isNullable: true,
},
{
name: 'redirectUris',
type: 'text',
},
{
name: 'postLogoutRedirectUris',
type: 'text',
isNullable: true,
},
{
name: 'tokenEndpointAuthMethod',
type: 'text',
isNullable: true,
},
{
name: 'grantTypes',
type: 'text',
isNullable: true,
},
{
name: 'responseTypes',
type: 'text',
isNullable: true,
},
{
name: 'public',
type: 'boolean',
isNullable: true,
},
{
name: 'type',
type: 'text',
isNullable: true,
},
{
name: 'requirePKCE',
type: 'boolean',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'metadata',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthClient',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthClient');
}
}
@@ -0,0 +1,77 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthConsent1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthConsent',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthConsent');
}
}
@@ -0,0 +1,106 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthRefreshToken1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthRefreshToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'revoked',
type: 'datetime',
isNullable: true,
},
{
name: 'authTime',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthRefreshToken');
}
}
@@ -0,0 +1,99 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreatePasskey1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'passkey',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'credentialID',
type: 'text',
},
{
name: 'counter',
type: 'integer',
},
{
name: 'deviceType',
type: 'text',
},
{
name: 'backedUp',
type: 'boolean',
},
{
name: 'transports',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'aaguid',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'passkey',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_credentialID_idx',
columnNames: ['credentialID'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('passkey');
}
}
@@ -0,0 +1,79 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateSession1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'session',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
{
name: 'ipAddress',
type: 'text',
isNullable: true,
},
{
name: 'userAgent',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
],
}),
);
await queryRunner.createIndex(
'session',
new TableIndex({
name: 'session_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'session',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('session');
}
}
@@ -0,0 +1,58 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateUser1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
isUnique: true,
},
{
name: 'emailVerified',
type: 'boolean',
default: false,
},
{
name: 'image',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
@@ -0,0 +1,59 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateVerification1777217882945 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'verification',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'identifier',
type: 'text',
},
{
name: 'value',
type: 'text',
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
await queryRunner.createIndex(
'verification',
new TableIndex({
name: 'verification_identifier_idx',
columnNames: ['identifier'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('verification');
}
}
@@ -0,0 +1,103 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateAccount1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'account',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'accountId',
type: 'text',
},
{
name: 'providerId',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'accessToken',
type: 'text',
isNullable: true,
},
{
name: 'refreshToken',
type: 'text',
isNullable: true,
},
{
name: 'idToken',
type: 'text',
isNullable: true,
},
{
name: 'accessTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'refreshTokenExpiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scope',
type: 'text',
isNullable: true,
},
{
name: 'password',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
],
}),
);
await queryRunner.createIndex(
'account',
new TableIndex({
name: 'account_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'account',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('account');
}
}
@@ -0,0 +1,46 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateJwks1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'jwks',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'privateKey',
type: 'text',
},
{
name: 'createdAt',
type: 'datetime',
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('jwks');
}
}
@@ -0,0 +1,113 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthAccessToken1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthAccessToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isNullable: true,
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'refreshId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthAccessToken',
new TableForeignKey({
columnNames: ['refreshId'],
referencedTableName: 'oauthRefreshToken',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthAccessToken');
}
}
@@ -0,0 +1,184 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthClient1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthClient',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
isUnique: true,
},
{
name: 'clientSecret',
type: 'text',
isNullable: true,
},
{
name: 'disabled',
type: 'boolean',
isNullable: true,
default: false,
},
{
name: 'skipConsent',
type: 'boolean',
isNullable: true,
},
{
name: 'enableEndSession',
type: 'boolean',
isNullable: true,
},
{
name: 'subjectType',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'uri',
type: 'text',
isNullable: true,
},
{
name: 'icon',
type: 'text',
isNullable: true,
},
{
name: 'contacts',
type: 'text',
isNullable: true,
},
{
name: 'tos',
type: 'text',
isNullable: true,
},
{
name: 'policy',
type: 'text',
isNullable: true,
},
{
name: 'softwareId',
type: 'text',
isNullable: true,
},
{
name: 'softwareVersion',
type: 'text',
isNullable: true,
},
{
name: 'softwareStatement',
type: 'text',
isNullable: true,
},
{
name: 'redirectUris',
type: 'text',
},
{
name: 'postLogoutRedirectUris',
type: 'text',
isNullable: true,
},
{
name: 'tokenEndpointAuthMethod',
type: 'text',
isNullable: true,
},
{
name: 'grantTypes',
type: 'text',
isNullable: true,
},
{
name: 'responseTypes',
type: 'text',
isNullable: true,
},
{
name: 'public',
type: 'boolean',
isNullable: true,
},
{
name: 'type',
type: 'text',
isNullable: true,
},
{
name: 'requirePKCE',
type: 'boolean',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'metadata',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthClient',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthClient');
}
}
@@ -0,0 +1,77 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthConsent1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthConsent',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'userId',
type: 'text',
isNullable: true,
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'updatedAt',
type: 'datetime',
isNullable: true,
},
],
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthConsent',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthConsent');
}
}
@@ -0,0 +1,106 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateOauthRefreshToken1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'oauthRefreshToken',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'clientId',
type: 'text',
},
{
name: 'sessionId',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
{
name: 'referenceId',
type: 'text',
isNullable: true,
},
{
name: 'expiresAt',
type: 'datetime',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'revoked',
type: 'datetime',
isNullable: true,
},
{
name: 'authTime',
type: 'datetime',
isNullable: true,
},
{
name: 'scopes',
type: 'text',
},
],
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['clientId'],
referencedTableName: 'oauthClient',
referencedColumnNames: ['clientId'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['sessionId'],
referencedTableName: 'session',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createForeignKey(
'oauthRefreshToken',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('oauthRefreshToken');
}
}
@@ -0,0 +1,99 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreatePasskey1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'passkey',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: true,
},
{
name: 'publicKey',
type: 'text',
},
{
name: 'userId',
type: 'text',
},
{
name: 'credentialID',
type: 'text',
},
{
name: 'counter',
type: 'integer',
},
{
name: 'deviceType',
type: 'text',
},
{
name: 'backedUp',
type: 'boolean',
},
{
name: 'transports',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
isNullable: true,
},
{
name: 'aaguid',
type: 'text',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'passkey',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createIndex(
'passkey',
new TableIndex({
name: 'passkey_credentialID_idx',
columnNames: ['credentialID'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('passkey');
}
}
@@ -0,0 +1,79 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateSession1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'session',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'token',
type: 'text',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
},
{
name: 'ipAddress',
type: 'text',
isNullable: true,
},
{
name: 'userAgent',
type: 'text',
isNullable: true,
},
{
name: 'userId',
type: 'text',
},
],
}),
);
await queryRunner.createIndex(
'session',
new TableIndex({
name: 'session_userId_idx',
columnNames: ['userId'],
}),
);
await queryRunner.createForeignKey(
'session',
new TableForeignKey({
columnNames: ['userId'],
referencedTableName: 'user',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('session');
}
}
@@ -0,0 +1,58 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateUser1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
isUnique: true,
},
{
name: 'emailVerified',
type: 'boolean',
default: false,
},
{
name: 'image',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
@@ -0,0 +1,59 @@
import {
type MigrationInterface,
type QueryRunner,
Table,
TableColumn,
TableForeignKey,
TableIndex,
} from 'typeorm';
export class CreateVerification1777217895075 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'verification',
columns: [
{
name: 'id',
type: 'text',
isPrimary: true,
},
{
name: 'identifier',
type: 'text',
},
{
name: 'value',
type: 'text',
},
{
name: 'expiresAt',
type: 'datetime',
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
await queryRunner.createIndex(
'verification',
new TableIndex({
name: 'verification_identifier_idx',
columnNames: ['identifier'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('verification');
}
}
+8
View File
@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bodyParser: false });
await app.listen(process.env.PORT ?? 3001);
}
bootstrap();
@@ -0,0 +1,34 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { IntBaseEntity } from '../base/BaseEntity';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Entity('bucket_list')
export class BucketList extends IntBaseEntity {
@Column({ name: 'name' })
name: string;
@Column({ name: 'lat', nullable: true })
lat: number | null;
@Column({ name: 'lng', nullable: true })
lng: number | null;
@Column({ name: 'country_code', nullable: true })
countryCode: string | null;
@Column('text', { name: 'notes', nullable: true })
notes: string | null;
@Column({
name: 'target_date',
nullable: true,
default: null,
})
targetDate: string | null;
@ManyToOne(() => SqliteUsers, (users) => users.bucketLists, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,15 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { IntBaseEntity } from '../base/BaseEntity';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Entity('visited_countries')
export class VisitedCountries extends IntBaseEntity {
@Column('text', { name: 'country_code' })
countryCode: string;
@ManyToOne(() => SqliteUsers, (users) => users.visitedCountries, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,22 @@
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { IntBaseEntity } from '../base/BaseEntity';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Index('idx_visited_regions_country', ['countryCode'], {})
@Entity('visited_regions')
export class VisitedRegions extends IntBaseEntity {
@Column({ name: 'region_code' })
regionCode: string;
@Column({ name: 'region_name' })
regionName: string;
@Column({ name: 'country_code' })
countryCode: string;
@ManyToOne(() => SqliteUsers, (users) => users.visitedRegions, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,59 @@
/**
* AUTOGENERATED
*/
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { User } from './User';
@Entity('account')
export class Account {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'accountId' })
accountId: string;
@Column('text', { name: 'providerId' })
providerId: string;
@Index('account_userId_idx')
@Column('text', { name: 'userId' })
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user: User;
@Column('text', { name: 'accessToken', nullable: true })
accessToken: string | null;
@Column('text', { name: 'refreshToken', nullable: true })
refreshToken: string | null;
@Column('text', { name: 'idToken', nullable: true })
idToken: string | null;
@Column({ type: 'timestamp', name: 'accessTokenExpiresAt', nullable: true })
accessTokenExpiresAt: Date | null;
@Column({ type: 'timestamp', name: 'refreshTokenExpiresAt', nullable: true })
refreshTokenExpiresAt: Date | null;
@Column('text', { name: 'scope', nullable: true })
scope: string | null;
@Column('text', { name: 'password', nullable: true })
password: string | null;
@Column({ type: 'timestamp', name: 'createdAt' })
createdAt: Date;
@Column({ type: 'timestamp', name: 'updatedAt' })
updatedAt: Date;
}
@@ -0,0 +1,22 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('jwks')
export class Jwks {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'publicKey' })
publicKey: string;
@Column('text', { name: 'privateKey' })
privateKey: string;
@Column({ type: 'timestamp', name: 'createdAt' })
createdAt: Date;
@Column({ type: 'timestamp', name: 'expiresAt', nullable: true })
expiresAt: Date | null;
}
@@ -0,0 +1,57 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { OauthClient } from './OauthClient';
import { OauthRefreshToken } from './OauthRefreshToken';
import { Session } from './Session';
import { User } from './User';
@Entity('oauthAccessToken')
export class OauthAccessToken {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'token', nullable: true, unique: true })
token: string | null;
@Column('text', { name: 'clientId' })
clientId: string;
@ManyToOne(() => OauthClient, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'clientId', referencedColumnName: 'clientId' })
client: OauthClient;
@Column('text', { name: 'sessionId', nullable: true })
sessionId: string | null;
@ManyToOne(() => Session, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'sessionId', referencedColumnName: 'id' })
session?: Session;
@Column('text', { name: 'userId', nullable: true })
userId: string | null;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user?: User;
@Column('text', { name: 'referenceId', nullable: true })
referenceId: string | null;
@Column('text', { name: 'refreshId', nullable: true })
refreshId: string | null;
@ManyToOne(() => OauthRefreshToken, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'refreshId', referencedColumnName: 'id' })
refresh?: OauthRefreshToken;
@Column({ type: 'timestamp', name: 'expiresAt', nullable: true })
expiresAt: Date | null;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column('text', { name: 'scopes' })
scopes: string;
}
@@ -0,0 +1,56 @@
/**
* AUTOGENERATED
*/
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { User } from './User';
@Entity('oauthApplication')
export class OauthApplication {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'name', nullable: true })
name: string | null;
@Column('text', { name: 'icon', nullable: true })
icon: string | null;
@Column('text', { name: 'metadata', nullable: true })
metadata: string | null;
@Column('text', { name: 'clientId', nullable: true, unique: true })
clientId: string | null;
@Column('text', { name: 'clientSecret', nullable: true })
clientSecret: string | null;
@Column('text', { name: 'redirectUrls', nullable: true })
redirectUrls: string | null;
@Column('text', { name: 'type', nullable: true })
type: string | null;
@Column('boolean', { name: 'disabled', nullable: true, default: false })
disabled: boolean | null;
@Index('oauthApplication_userId_idx')
@Column('text', { name: 'userId', nullable: true })
userId: string | null;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user?: User;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column({ type: 'timestamp', name: 'updatedAt', nullable: true })
updatedAt: Date | null;
}
@@ -0,0 +1,102 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { User } from './User';
@Entity('oauthClient')
export class OauthClient {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'clientId', unique: true })
clientId: string;
@Column('text', { name: 'clientSecret', nullable: true })
clientSecret: string | null;
@Column('boolean', { name: 'disabled', nullable: true, default: false })
disabled: boolean | null;
@Column('boolean', { name: 'skipConsent', nullable: true })
skipConsent: boolean | null;
@Column('boolean', { name: 'enableEndSession', nullable: true })
enableEndSession: boolean | null;
@Column('text', { name: 'subjectType', nullable: true })
subjectType: string | null;
@Column('text', { name: 'scopes', nullable: true })
scopes: string | null;
@Column('text', { name: 'userId', nullable: true })
userId: string | null;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user?: User;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column({ type: 'timestamp', name: 'updatedAt', nullable: true })
updatedAt: Date | null;
@Column('text', { name: 'name', nullable: true })
name: string | null;
@Column('text', { name: 'uri', nullable: true })
uri: string | null;
@Column('text', { name: 'icon', nullable: true })
icon: string | null;
@Column('text', { name: 'contacts', nullable: true })
contacts: string | null;
@Column('text', { name: 'tos', nullable: true })
tos: string | null;
@Column('text', { name: 'policy', nullable: true })
policy: string | null;
@Column('text', { name: 'softwareId', nullable: true })
softwareId: string | null;
@Column('text', { name: 'softwareVersion', nullable: true })
softwareVersion: string | null;
@Column('text', { name: 'softwareStatement', nullable: true })
softwareStatement: string | null;
@Column('text', { name: 'redirectUris' })
redirectUris: string;
@Column('text', { name: 'postLogoutRedirectUris', nullable: true })
postLogoutRedirectUris: string | null;
@Column('text', { name: 'tokenEndpointAuthMethod', nullable: true })
tokenEndpointAuthMethod: string | null;
@Column('text', { name: 'grantTypes', nullable: true })
grantTypes: string | null;
@Column('text', { name: 'responseTypes', nullable: true })
responseTypes: string | null;
@Column('boolean', { name: 'public', nullable: true })
public: boolean | null;
@Column('text', { name: 'type', nullable: true })
type: string | null;
@Column('boolean', { name: 'requirePKCE', nullable: true })
requirePKCE: boolean | null;
@Column('text', { name: 'referenceId', nullable: true })
referenceId: string | null;
@Column('text', { name: 'metadata', nullable: true })
metadata: string | null;
}
@@ -0,0 +1,38 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { OauthClient } from './OauthClient';
import { User } from './User';
@Entity('oauthConsent')
export class OauthConsent {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'clientId' })
clientId: string;
@ManyToOne(() => OauthClient, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'clientId', referencedColumnName: 'clientId' })
client: OauthClient;
@Column('text', { name: 'userId', nullable: true })
userId: string | null;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user?: User;
@Column('text', { name: 'referenceId', nullable: true })
referenceId: string | null;
@Column('text', { name: 'scopes' })
scopes: string;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column({ type: 'timestamp', name: 'updatedAt', nullable: true })
updatedAt: Date | null;
}
@@ -0,0 +1,55 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { OauthClient } from './OauthClient';
import { Session } from './Session';
import { User } from './User';
@Entity('oauthRefreshToken')
export class OauthRefreshToken {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'token', unique: true })
token: string;
@Column('text', { name: 'clientId' })
clientId: string;
@ManyToOne(() => OauthClient, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'clientId', referencedColumnName: 'clientId' })
client: OauthClient;
@Column('text', { name: 'sessionId', nullable: true })
sessionId: string | null;
@ManyToOne(() => Session, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'sessionId', referencedColumnName: 'id' })
session?: Session;
@Column('text', { name: 'userId' })
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user: User;
@Column('text', { name: 'referenceId', nullable: true })
referenceId: string | null;
@Column({ type: 'timestamp', name: 'expiresAt', nullable: true })
expiresAt: Date | null;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column({ type: 'timestamp', name: 'revoked', nullable: true })
revoked: Date | null;
@Column({ type: 'timestamp', name: 'authTime', nullable: true })
authTime: Date | null;
@Column('text', { name: 'scopes' })
scopes: string;
}
@@ -0,0 +1,54 @@
/**
* AUTOGENERATED
*/
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { User } from './User';
@Entity('passkey')
export class Passkey {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'name', nullable: true })
name: string | null;
@Column('text', { name: 'publicKey' })
publicKey: string;
@Index('passkey_userId_idx')
@Column('text', { name: 'userId' })
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user: User;
@Index('passkey_credentialID_idx')
@Column('text', { name: 'credentialID' })
credentialID: string;
@Column('integer', { name: 'counter' })
counter: number;
@Column('text', { name: 'deviceType' })
deviceType: string;
@Column('boolean', { name: 'backedUp' })
backedUp: boolean;
@Column('text', { name: 'transports', nullable: true })
transports: string | null;
@Column({ type: 'timestamp', name: 'createdAt', nullable: true })
createdAt: Date | null;
@Column('text', { name: 'aaguid', nullable: true })
aaguid: string | null;
}
@@ -0,0 +1,44 @@
/**
* AUTOGENERATED
*/
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { User } from './User';
@Entity('session')
export class Session {
@PrimaryColumn('text')
id: string;
@Column({ type: 'timestamp', name: 'expiresAt' })
expiresAt: Date;
@Column('text', { name: 'token', unique: true })
token: string;
@Column({ type: 'timestamp', name: 'createdAt' })
createdAt: Date;
@Column({ type: 'timestamp', name: 'updatedAt' })
updatedAt: Date;
@Column('text', { name: 'ipAddress', nullable: true })
ipAddress: string | null;
@Column('text', { name: 'userAgent', nullable: true })
userAgent: string | null;
@Index('session_userId_idx')
@Column('text', { name: 'userId' })
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'userId', referencedColumnName: 'id' })
user: User;
}
@@ -0,0 +1,28 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('user')
export class User {
@PrimaryColumn('text')
id: string;
@Column('text', { name: 'name' })
name: string;
@Column('text', { name: 'email', unique: true })
email: string;
@Column('boolean', { name: 'emailVerified', default: false })
emailVerified: boolean;
@Column('text', { name: 'image', nullable: true })
image: string | null;
@Column({ type: 'timestamp', name: 'createdAt' })
createdAt: Date;
@Column({ type: 'timestamp', name: 'updatedAt' })
updatedAt: Date;
}
@@ -0,0 +1,26 @@
/**
* AUTOGENERATED
*/
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity('verification')
export class Verification {
@PrimaryColumn('text')
id: string;
@Index('verification_identifier_idx')
@Column('text', { name: 'identifier' })
identifier: string;
@Column('text', { name: 'value' })
value: string;
@Column({ type: 'timestamp', name: 'expiresAt' })
expiresAt: Date;
@Column({ type: 'timestamp', name: 'createdAt' })
createdAt: Date;
@Column({ type: 'timestamp', name: 'updatedAt' })
updatedAt: Date;
}
@@ -0,0 +1,62 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
PrimaryColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
// Layer 1 — timestamps only, always present
export abstract class TimestampedEntity {
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
export abstract class TimestampedSortableEntity extends TimestampedEntity {
@Column('int', { name: 'sort_order', default: 0 })
sortOrder: number;
}
// Layer 2 — adds soft delete
export abstract class SoftDeletableEntity extends TimestampedEntity {
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null;
}
// Layer 3a — adds integer PK to timestamped entity
export abstract class IntBaseEntity extends TimestampedEntity {
@PrimaryGeneratedColumn()
id: number;
}
export abstract class IntSortableBaseEntity extends IntBaseEntity {
@Column('int', { name: 'sort_order', default: 0 })
sortOrder: number;
}
// Layer 3b — adds integer PK + soft delete
export abstract class SoftDeletableIntBaseEntity extends SoftDeletableEntity {
@PrimaryGeneratedColumn()
id: number;
}
// Layer 3c — natural/business string PK (e.g. country code, slug)
export abstract class StringBaseEntity extends TimestampedEntity {
@PrimaryColumn({ name: 'id' })
id: string;
}
export abstract class StringSortableBaseEntity extends StringBaseEntity {
@Column('int', { name: 'sort_order', default: 0 })
sortOrder: number;
}
// Layer 3d — natural string PK + soft delete
export abstract class SoftDeletableStringBaseEntity extends SoftDeletableEntity {
@PrimaryColumn({ name: 'id' })
id: string;
}
@@ -0,0 +1,25 @@
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { IntBaseEntity } from '../base/BaseEntity';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Index('idx_mcp_tokens_hash', ['tokenHash'], { unique: true })
@Entity('mcp_tokens')
export class McpTokens extends IntBaseEntity {
@Column({ name: 'name' })
name: string;
@Column({ name: 'token_hash', unique: true })
tokenHash: string;
@Column({ name: 'token_prefix' })
tokenPrefix: string;
@Column({ name: 'last_used_at', nullable: true })
lastUsedAt: Date | null;
@ManyToOne(() => SqliteUsers, (users) => users.mcpTokens, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,35 @@
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
} from 'typeorm';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Index('idx_ncp_user', ['userId'], {})
@Entity('notification_channel_preferences')
export class NotificationChannelPreferences {
@PrimaryColumn('int', { name: 'user_id' })
userId: number;
@PrimaryColumn({ name: 'event_type' })
eventType: string;
@PrimaryColumn({ name: 'channel' })
channel: string;
@Column({ name: 'enabled', default: true })
enabled: boolean;
@ManyToOne(
() => SqliteUsers,
(users) => users.notificationChannelPreferences,
{
onDelete: 'CASCADE',
},
)
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,81 @@
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { IntBaseEntity } from '../base/BaseEntity';
import { SqliteUsers } from '../old-entities/SqliteUsers';
@Index('idx_notifications_target_scope', ['target', 'scope'], {})
@Index('idx_notifications_recipient_created', ['recipientId', 'createdAt'], {})
@Index(
'idx_notifications_recipient',
['recipientId', 'isRead', 'createdAt'],
{},
)
@Entity('notifications')
export class Notifications extends IntBaseEntity {
@Column({ name: 'type' })
type: string;
@Column({ name: 'scope' })
scope: string;
@Column('int', { name: 'target' })
target: number;
@Column('int', { name: 'recipient_id' })
recipientId: number;
@Column({ name: 'title_key' })
titleKey: string;
@Column('simple-json', {
name: 'title_params',
nullable: true,
default: {},
})
titleParams: Record<string, unknown> | null;
@Column({ name: 'text_key' })
textKey: string;
@Column('simple-json', {
name: 'text_params',
nullable: true,
default: {},
})
textParams: Record<string, unknown> | null;
@Column({ name: 'positive_text_key', nullable: true })
positiveTextKey: string | null;
@Column({ name: 'negative_text_key', nullable: true })
negativeTextKey: string | null;
@Column({ name: 'positive_callback', nullable: true })
positiveCallback: string | null;
@Column({ name: 'negative_callback', nullable: true })
negativeCallback: string | null;
@Column({ name: 'response', nullable: true })
response: string | null;
@Column({ name: 'navigate_text_key', nullable: true })
navigateTextKey: string | null;
@Column({ name: 'navigate_target', nullable: true })
navigateTarget: string | null;
@Column({ name: 'is_read', nullable: true, default: false })
isRead: boolean | null;
@ManyToOne(() => SqliteUsers, (users) => users.notifications, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'recipient_id', referencedColumnName: 'id' }])
recipient: SqliteUsers;
@ManyToOne(() => SqliteUsers, (users) => users.notifications2, {
onDelete: 'SET NULL',
})
@JoinColumn([{ name: 'sender_id', referencedColumnName: 'id' }])
sender: SqliteUsers;
}
@@ -0,0 +1,53 @@
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
} from 'typeorm';
import { OldOauthConsents } from './OldOauthConsents';
import { OldOauthTokens } from './OldOauthTokens';
import { SqliteUsers } from './SqliteUsers';
import { StringBaseEntity } from '../base/BaseEntity';
@Index('idx_oauth_clients_client_id', ['clientId'], { unique: true })
@Index('idx_oauth_clients_user', ['userId'], {})
@Entity('oauth_clients')
export class OldOauthClients extends StringBaseEntity {
@Column('int', { name: 'user_id', nullable: true })
userId: number | null;
@Column({ name: 'name' })
name: string;
@Column({ name: 'client_id', unique: true })
clientId: string;
@Column({ name: 'client_secret_hash' })
clientSecretHash: string;
@Column('simple-array', { name: 'redirect_uris', default: [] })
redirectUris: string[];
@Column('simple-array', { name: 'allowed_scopes', default: [] })
allowedScopes: string[];
@Column({ name: 'is_public', default: false })
isPublic: boolean;
@Column({ name: 'created_via', default: 'settings_ui' })
createdVia: string;
@OneToMany(() => OldOauthConsents, (oauthConsents) => oauthConsents.client)
oauthConsents: OldOauthConsents[];
@OneToMany(() => OldOauthTokens, (oauthTokens) => oauthTokens.client)
oauthTokens: OldOauthTokens[];
@ManyToOne(() => SqliteUsers, (users) => users.oauthClients, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
}
@@ -0,0 +1,26 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { SqliteUsers } from './SqliteUsers';
import { OldOauthClients } from './OldOauthClients';
import { IntBaseEntity } from '../base/BaseEntity';
@Entity('oauth_consents')
export class OldOauthConsents extends IntBaseEntity {
@Column('simple-array', { name: 'scopes', default: [] })
scopes: string[];
@ManyToOne(() => SqliteUsers, (users) => users.oauthConsents, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
@ManyToOne(
() => OldOauthClients,
(oauthClients) => oauthClients.oauthConsents,
{
onDelete: 'CASCADE',
},
)
@JoinColumn([{ name: 'client_id', referencedColumnName: 'clientId' }])
client: OldOauthClients;
}
@@ -0,0 +1,68 @@
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
} from 'typeorm';
import { SqliteUsers } from './SqliteUsers';
import { OldOauthClients } from './OldOauthClients';
import { IntBaseEntity } from '../base/BaseEntity';
@Index('idx_oauth_tokens_parent', ['parentTokenId'], {})
@Index('idx_oauth_tokens_refresh', ['refreshTokenHash'], { unique: true })
@Index('idx_oauth_tokens_access', ['accessTokenHash'], { unique: true })
@Index('idx_oauth_tokens_user', ['userId'], {})
@Entity('oauth_tokens')
export class OldOauthTokens extends IntBaseEntity {
@Column('int', { name: 'user_id' })
userId: number;
@Column({ name: 'access_token_hash', unique: true })
accessTokenHash: string;
@Column({ name: 'refresh_token_hash', unique: true })
refreshTokenHash: string;
@Column('simple-array', { name: 'scopes', default: [] })
scopes: string[];
@Column('datetime', { name: 'access_token_expires_at' })
accessTokenExpiresAt: Date;
@Column('datetime', { name: 'refresh_token_expires_at' })
refreshTokenExpiresAt: Date;
@Column('datetime', { name: 'revoked_at', nullable: true })
revokedAt: Date | null;
@Column('int', { name: 'parent_token_id', nullable: true })
parentTokenId: number | null;
@Column({ name: 'audience', nullable: true })
audience: string | null;
@ManyToOne(() => OldOauthTokens, (oauthTokens) => oauthTokens.oauthTokens)
@JoinColumn([{ name: 'parent_token_id', referencedColumnName: 'id' }])
parentToken: OldOauthTokens;
@OneToMany(() => OldOauthTokens, (oauthTokens) => oauthTokens.parentToken)
oauthTokens: OldOauthTokens[];
@ManyToOne(() => SqliteUsers, (users) => users.oauthTokens, {
onDelete: 'CASCADE',
})
@JoinColumn([{ name: 'user_id', referencedColumnName: 'id' }])
user: SqliteUsers;
@ManyToOne(
() => OldOauthClients,
(oauthClients) => oauthClients.oauthTokens,
{
onDelete: 'CASCADE',
},
)
@JoinColumn([{ name: 'client_id', referencedColumnName: 'clientId' }])
client: OldOauthClients;
}

Some files were not shown because too many files have changed in this diff Show More