mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b53948231 | |||
| 78d6f2ba77 | |||
| bb89d70a94 | |||
| ad9f3887d8 | |||
| 7f1fb508db | |||
| 1f5deeba6c | |||
| ca832e8d88 | |||
| 12fc7f7b68 | |||
| 2770a189df | |||
| 2b162a8cc7 | |||
| 009d89fecf | |||
| 5c3b89578d | |||
| 303e7de433 | |||
| 08eb7f3733 | |||
| 90d86eda61 | |||
| 0eca6d54a1 | |||
| bc1fb71391 | |||
| cb425fb397 | |||
| 35ed712d46 | |||
| 4923973380 | |||
| 8342cf3010 | |||
| 2a37eeccb3 | |||
| ae0e59d9f1 | |||
| 50bb7573fd | |||
| b852317c84 | |||
| 4436b6f673 | |||
| 311647fd46 | |||
| 28dbd86d03 | |||
| 842d9760df | |||
| 58218ff5f6 | |||
| 83be5fc92a | |||
| 7798d2a3fd | |||
| ec1ed60117 | |||
| ed4c21eade | |||
| 9093948ff6 | |||
| 2cea4d73aa | |||
| a2a6f52e6e | |||
| 0978b40b6d | |||
| 6155b6dc86 | |||
| 314486325e | |||
| 523bca3a20 | |||
| d5be528d4b |
@@ -62,6 +62,7 @@ body:
|
||||
- Docker (standalone)
|
||||
- Kubernetes / Helm
|
||||
- Unraid template
|
||||
- Proxmox Community Script
|
||||
- Sources
|
||||
- Other
|
||||
validations:
|
||||
|
||||
@@ -26,6 +26,9 @@ jobs:
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
for (const pull of pulls) {
|
||||
const hasBypass = pull.labels.some(l => l.name === 'bypass-branch-check');
|
||||
if (hasBypass) continue;
|
||||
|
||||
const hasLabel = pull.labels.some(l => l.name === 'wrong-base-branch');
|
||||
if (!hasLabel) continue;
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ on:
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
- 'wiki/**'
|
||||
- '.github/workflows/wiki.yml'
|
||||
- '.github/workflows/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/PULL_REQUEST_TEMPLATE.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
|
||||
@@ -21,6 +21,12 @@ jobs:
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
// bypass-branch-check label skips all enforcement
|
||||
if (labels.includes('bypass-branch-check')) {
|
||||
console.log('bypass-branch-check label present, skipping enforcement.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the base was fixed, remove the label and let it through
|
||||
if (base !== 'main') {
|
||||
if (labels.includes('wrong-base-branch')) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: trek
|
||||
version: 3.0.9
|
||||
version: 3.0.12
|
||||
description: Minimal Helm chart for TREK app
|
||||
appVersion: "3.0.9"
|
||||
appVersion: "3.0.12"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trek-client",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trek-client",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.12",
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"axios": "^1.6.7",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trek-client",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ function ProtectedRoute({ children, adminRequired = false, addonId }: ProtectedR
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
const redirectParam = encodeURIComponent(location.pathname + location.search)
|
||||
const redirectParam = encodeURIComponent(location.pathname + location.search + location.hash)
|
||||
return <Navigate to={`/login?redirect=${redirectParam}`} replace />
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ apiClient.interceptors.response.use(
|
||||
if (error.response?.status === 401 && (error.response?.data as { code?: string } | undefined)?.code === 'AUTH_REQUIRED') {
|
||||
const { pathname } = window.location
|
||||
if (!isAuthPublicPath(pathname)) {
|
||||
const currentPath = pathname + window.location.search
|
||||
const currentPath = pathname + window.location.search + window.location.hash
|
||||
window.location.href = '/login?redirect=' + encodeURIComponent(currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { Upload, Trash2, ExternalLink, Download, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { Upload, Trash2, ExternalLink, Download, X, FileText, FileImage, File, MapPin, Ticket, StickyNote, Star, RotateCcw, Pencil, Check, ChevronLeft, ChevronRight, Plane, Train, Car, Ship } from 'lucide-react'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { filesApi } from '../../api/client'
|
||||
@@ -236,6 +236,15 @@ function AvatarChip({ name, avatarUrl, size = 20 }: { name: string; avatarUrl?:
|
||||
)
|
||||
}
|
||||
|
||||
const TRANSPORT_TYPES = new Set(['flight', 'train', 'car', 'cruise'])
|
||||
|
||||
function transportIcon(type: string) {
|
||||
if (type === 'train') return Train
|
||||
if (type === 'car') return Car
|
||||
if (type === 'cruise') return Ship
|
||||
return Plane
|
||||
}
|
||||
|
||||
interface FileManagerProps {
|
||||
files?: TripFile[]
|
||||
onUpload: (fd: FormData) => Promise<any>
|
||||
@@ -490,7 +499,9 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
|
||||
<SourceBadge key={p.id} icon={MapPin} label={`${t('files.sourcePlan')} · ${p.name}`} />
|
||||
))}
|
||||
{linkedReservations.map(r => (
|
||||
<SourceBadge key={r.id} icon={Ticket} label={`${t('files.sourceBooking')} · ${r.title || t('files.sourceBooking')}`} />
|
||||
TRANSPORT_TYPES.has(r.type)
|
||||
? <SourceBadge key={r.id} icon={transportIcon(r.type)} label={`${t('files.sourceTransport')} · ${r.title || t('files.sourceTransport')}`} />
|
||||
: <SourceBadge key={r.id} icon={Ticket} label={`${t('files.sourceBooking')} · ${r.title || t('files.sourceBooking')}`} />
|
||||
))}
|
||||
{file.note_id && (
|
||||
<SourceBadge icon={StickyNote} label={t('files.sourceCollab') || 'Collab Notes'} />
|
||||
@@ -673,52 +684,68 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
|
||||
</div>
|
||||
)
|
||||
|
||||
const bookingReservations = reservations.filter(r => !TRANSPORT_TYPES.has(r.type))
|
||||
const transportReservations = reservations.filter(r => TRANSPORT_TYPES.has(r.type))
|
||||
|
||||
const reservationBtn = (r: Reservation) => {
|
||||
const isLinked = file.reservation_id === r.id || (file.linked_reservation_ids || []).includes(r.id)
|
||||
const Icon = TRANSPORT_TYPES.has(r.type) ? transportIcon(r.type) : Ticket
|
||||
return (
|
||||
<button key={r.id} onClick={async () => {
|
||||
if (isLinked) {
|
||||
if (file.reservation_id === r.id) {
|
||||
await handleAssign(file.id, { reservation_id: null })
|
||||
} else {
|
||||
try {
|
||||
const linksRes = await filesApi.getLinks(tripId, file.id)
|
||||
const link = (linksRes.links || []).find((l: any) => l.reservation_id === r.id)
|
||||
if (link) await filesApi.removeLink(tripId, file.id, link.id)
|
||||
refreshFiles()
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
if (!file.reservation_id) {
|
||||
await handleAssign(file.id, { reservation_id: r.id })
|
||||
} else {
|
||||
try {
|
||||
await filesApi.addLink(tripId, file.id, { reservation_id: r.id })
|
||||
refreshFiles()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}} style={{
|
||||
width: '100%', textAlign: 'left', padding: '6px 10px 6px 20px', background: isLinked ? 'var(--bg-hover)' : 'none',
|
||||
border: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--text-primary)',
|
||||
borderRadius: 8, fontFamily: 'inherit', fontWeight: isLinked ? 600 : 400,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = isLinked ? 'var(--bg-hover)' : 'transparent'}>
|
||||
<Icon size={12} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title || r.name}</span>
|
||||
{isLinked && <Check size={14} style={{ marginLeft: 'auto', flexShrink: 0, color: 'var(--accent)' }} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const bookingsSection = reservations.length > 0 && (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-faint)', padding: '8px 10px 4px', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
{t('files.assignBooking')}
|
||||
</div>
|
||||
{reservations.map(r => {
|
||||
const isLinked = file.reservation_id === r.id || (file.linked_reservation_ids || []).includes(r.id)
|
||||
return (
|
||||
<button key={r.id} onClick={async () => {
|
||||
if (isLinked) {
|
||||
// Unlink: if primary reservation_id, clear it; if via file_links, remove link
|
||||
if (file.reservation_id === r.id) {
|
||||
await handleAssign(file.id, { reservation_id: null })
|
||||
} else {
|
||||
try {
|
||||
const linksRes = await filesApi.getLinks(tripId, file.id)
|
||||
const link = (linksRes.links || []).find((l: any) => l.reservation_id === r.id)
|
||||
if (link) await filesApi.removeLink(tripId, file.id, link.id)
|
||||
refreshFiles()
|
||||
} catch {}
|
||||
}
|
||||
} else {
|
||||
// Link: if no primary, set it; otherwise use file_links
|
||||
if (!file.reservation_id) {
|
||||
await handleAssign(file.id, { reservation_id: r.id })
|
||||
} else {
|
||||
try {
|
||||
await filesApi.addLink(tripId, file.id, { reservation_id: r.id })
|
||||
refreshFiles()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}} style={{
|
||||
width: '100%', textAlign: 'left', padding: '6px 10px 6px 20px', background: isLinked ? 'var(--bg-hover)' : 'none',
|
||||
border: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--text-primary)',
|
||||
borderRadius: 8, fontFamily: 'inherit', fontWeight: isLinked ? 600 : 400,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = isLinked ? 'var(--bg-hover)' : 'transparent'}>
|
||||
<Ticket size={12} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title || r.name}</span>
|
||||
{isLinked && <Check size={14} style={{ marginLeft: 'auto', flexShrink: 0, color: 'var(--accent)' }} />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{bookingReservations.length > 0 && (
|
||||
<>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-faint)', padding: '8px 10px 4px', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
{t('files.assignBooking')}
|
||||
</div>
|
||||
{bookingReservations.map(reservationBtn)}
|
||||
</>
|
||||
)}
|
||||
{transportReservations.length > 0 && (
|
||||
<>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-faint)', padding: '8px 10px 4px', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: bookingReservations.length > 0 ? 4 : 0 }}>
|
||||
{t('files.assignTransport')}
|
||||
</div>
|
||||
{transportReservations.map(reservationBtn)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, LogIn, LogOut, KeyRound, BedDouble, Utensils, Users, LucideIcon } from 'lucide-react'
|
||||
import { accommodationsApi, mapsApi } from '../../api/client'
|
||||
import type { Trip, Day, Place, Category, AssignmentsMap, DayNotesMap } from '../../types'
|
||||
import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder'
|
||||
|
||||
function renderLucideIcon(icon:LucideIcon, props = {}) {
|
||||
if (!_renderToStaticMarkup) return ''
|
||||
@@ -285,8 +286,12 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
|
||||
}).join('')
|
||||
|
||||
const accommodationsForDay = (accommodations.accommodations || []).filter(a =>
|
||||
days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id)
|
||||
).sort((a, b) => a.start_day_id - b.start_day_id)
|
||||
day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false
|
||||
).sort((a, b) => {
|
||||
const startA = days.find(d => d.id === a.start_day_id)
|
||||
const startB = days.find(d => d.id === b.start_day_id)
|
||||
return (startA ? getDayOrder(startA, days) : 0) - (startB ? getDayOrder(startB, days) : 0)
|
||||
})
|
||||
|
||||
const accommodationDetails = accommodationsForDay.map(item => {
|
||||
const isCheckIn = day.id === item.start_day_id
|
||||
|
||||
@@ -892,6 +892,277 @@ describe('DayDetailPanel', () => {
|
||||
expect(screen.getByText(/June|15/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── Accommodation date-range picker — non-monotonic day IDs (issue #889) ─────
|
||||
|
||||
// Builds the reporter's exact ID layout: day_number 1-9 → IDs 17-25, day_number 10-16 → IDs 1-7.
|
||||
// This happens after repeated trip-length changes via generateDays (no import/migration needed).
|
||||
function buildNonMonotonicDays() {
|
||||
return [
|
||||
buildDay({ id: 17, trip_id: 1, date: '2026-04-30' }),
|
||||
buildDay({ id: 18, trip_id: 1, date: '2026-05-01' }),
|
||||
buildDay({ id: 19, trip_id: 1, date: '2026-05-02' }),
|
||||
buildDay({ id: 20, trip_id: 1, date: '2026-05-03' }),
|
||||
buildDay({ id: 21, trip_id: 1, date: '2026-05-04' }),
|
||||
buildDay({ id: 22, trip_id: 1, date: '2026-05-05' }),
|
||||
buildDay({ id: 23, trip_id: 1, date: '2026-05-06' }),
|
||||
buildDay({ id: 24, trip_id: 1, date: '2026-05-07' }),
|
||||
buildDay({ id: 25, trip_id: 1, date: '2026-05-08' }),
|
||||
buildDay({ id: 1, trip_id: 1, date: '2026-05-09' }),
|
||||
buildDay({ id: 2, trip_id: 1, date: '2026-05-10' }),
|
||||
buildDay({ id: 3, trip_id: 1, date: '2026-05-11' }),
|
||||
buildDay({ id: 4, trip_id: 1, date: '2026-05-12' }),
|
||||
buildDay({ id: 5, trip_id: 1, date: '2026-05-13' }),
|
||||
buildDay({ id: 6, trip_id: 1, date: '2026-05-14' }),
|
||||
buildDay({ id: 7, trip_id: 1, date: '2026-05-15' }),
|
||||
];
|
||||
}
|
||||
|
||||
// Returns the two CustomSelect trigger buttons for start/end day pickers.
|
||||
// When no dropdown is open, these are the only globally-visible buttons whose textContent
|
||||
// matches /Day \d+/ (the main panel title is a div, not a button).
|
||||
// [0] = start trigger, [1] = end trigger (DOM source order).
|
||||
function getDayPickerTriggers() {
|
||||
return screen.getAllByRole('button').filter(b => /Day \d+/.test(b.textContent ?? ''));
|
||||
}
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-056: non-monotonic IDs — end picker does not clobber start-day', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
const place = buildPlace({ id: 50, name: 'Range Hotel' });
|
||||
let capturedBody: any;
|
||||
server.use(
|
||||
http.post('/api/trips/1/accommodations', async ({ request }) => {
|
||||
capturedBody = await request.json();
|
||||
return HttpResponse.json({
|
||||
accommodation: {
|
||||
id: 99, place_id: 50, place_name: 'Range Hotel', place_address: null,
|
||||
start_day_id: capturedBody.start_day_id, end_day_id: capturedBody.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={days[0]} days={days} places={[place]} />);
|
||||
await userEvent.click(await screen.findByText(/Add accommodation/i));
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Range Hotel/i }));
|
||||
|
||||
// Both triggers show "Day 1"; the second one is the end picker.
|
||||
await userEvent.click(getDayPickerTriggers()[1]);
|
||||
// Select "Day 16" (id=7) from the open dropdown — textContent starts with "Day 16".
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
// start must remain id 17 (day 1) — old code would clobber it to id 7 via Math.min
|
||||
expect(capturedBody?.start_day_id).toBe(17);
|
||||
expect(capturedBody?.end_day_id).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-057: non-monotonic IDs — start picker does not collapse end when start has high ID', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
const place = buildPlace({ id: 51, name: 'Span Hotel' });
|
||||
let capturedBody: any;
|
||||
server.use(
|
||||
http.post('/api/trips/1/accommodations', async ({ request }) => {
|
||||
capturedBody = await request.json();
|
||||
return HttpResponse.json({
|
||||
accommodation: {
|
||||
id: 100, place_id: 51, place_name: 'Span Hotel', place_address: null,
|
||||
start_day_id: capturedBody.start_day_id, end_day_id: capturedBody.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={days[0]} days={days} places={[place]} />);
|
||||
await userEvent.click(await screen.findByText(/Add accommodation/i));
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Span Hotel/i }));
|
||||
|
||||
// Set end to day 16 (id=7, low ID but last day by position).
|
||||
await userEvent.click(getDayPickerTriggers()[1]);
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);
|
||||
|
||||
// Set start to day 9 (id=25, high ID, but earlier by position than day 16).
|
||||
// Old code: Math.max(25, 7) = 25 → end collapses to day 9.
|
||||
// New code: position(id=25)=8 < position(id=7)=15 → end stays at 7 (day 16).
|
||||
await userEvent.click(getDayPickerTriggers()[0]);
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 9'))!);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedBody?.start_day_id).toBe(25); // day 9
|
||||
expect(capturedBody?.end_day_id).toBe(7); // day 16 — must NOT have collapsed
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-058: non-monotonic IDs — All days button sets correct first/last IDs', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
const place = buildPlace({ id: 52, name: 'Full Trip Hotel' });
|
||||
let capturedBody: any;
|
||||
server.use(
|
||||
http.post('/api/trips/1/accommodations', async ({ request }) => {
|
||||
capturedBody = await request.json();
|
||||
return HttpResponse.json({
|
||||
accommodation: {
|
||||
id: 101, place_id: 52, place_name: 'Full Trip Hotel', place_address: null,
|
||||
start_day_id: capturedBody.start_day_id, end_day_id: capturedBody.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={days[0]} days={days} places={[place]} />);
|
||||
await userEvent.click(await screen.findByText(/Add accommodation/i));
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Full Trip Hotel/i }));
|
||||
|
||||
// "All" is the day.allDays translation (en: "All") — the Apply-to-entire-trip button.
|
||||
// When categories=[] the category-filter "All" button is not rendered, so this is unique.
|
||||
await userEvent.click(screen.getByRole('button', { name: /^All$/i }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
// days[0].id=17 (first by position), days[15].id=7 (last by position)
|
||||
expect(capturedBody?.start_day_id).toBe(17);
|
||||
expect(capturedBody?.end_day_id).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-059: sequential IDs — end picker clamping still works (regression guard)', async () => {
|
||||
const seqDays = [
|
||||
buildDay({ id: 101, trip_id: 1, date: '2026-06-01' }),
|
||||
buildDay({ id: 102, trip_id: 1, date: '2026-06-02' }),
|
||||
buildDay({ id: 103, trip_id: 1, date: '2026-06-03' }),
|
||||
];
|
||||
const place = buildPlace({ id: 53, name: 'Seq Hotel' });
|
||||
let capturedBody: any;
|
||||
server.use(
|
||||
http.post('/api/trips/1/accommodations', async ({ request }) => {
|
||||
capturedBody = await request.json();
|
||||
return HttpResponse.json({
|
||||
accommodation: {
|
||||
id: 102, place_id: 53, place_name: 'Seq Hotel', place_address: null,
|
||||
start_day_id: capturedBody.start_day_id, end_day_id: capturedBody.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={seqDays[0]} days={seqDays} places={[place]} />);
|
||||
await userEvent.click(await screen.findByText(/Add accommodation/i));
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Seq Hotel/i }));
|
||||
|
||||
// Pick end = day 3 (id=103, position 2 > position 0 of start id=101).
|
||||
await userEvent.click(getDayPickerTriggers()[1]);
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 3'))!);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedBody?.start_day_id).toBe(101);
|
||||
expect(capturedBody?.end_day_id).toBe(103);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Post-save state filter — non-monotonic IDs (issue #889 follow-up) ────────
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-060: non-monotonic IDs — hotel stays visible after edit-save (issue #889 regression)', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
let getCallCount = 0;
|
||||
server.use(
|
||||
http.get('/api/trips/1/accommodations', () => {
|
||||
getCallCount++;
|
||||
const acc = getCallCount === 1
|
||||
// Initial load: single-day so old filter (17>=17 && 17<=17) passes — hotel visible, edit possible
|
||||
? { id: 1, place_id: 50, place_name: 'Span Hotel', place_address: null, start_day_id: 17, end_day_id: 17, check_in: null, check_out: null, confirmation: null }
|
||||
// Post-save relist: full span — old filter (17>=17 && 17<=7) would drop it, new code keeps it
|
||||
: { id: 1, place_id: 50, place_name: 'Span Hotel', place_address: null, start_day_id: 17, end_day_id: 7, check_in: null, check_out: null, confirmation: null };
|
||||
return HttpResponse.json({ accommodations: [acc] });
|
||||
}),
|
||||
http.put('/api/trips/1/accommodations/1', async ({ request }) => {
|
||||
const body = await request.json() as any;
|
||||
return HttpResponse.json({
|
||||
accommodation: { id: 1, place_id: 50, place_name: 'Span Hotel', place_address: null,
|
||||
start_day_id: body.start_day_id, end_day_id: body.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={days[0]} days={days} />);
|
||||
await screen.findByText('Span Hotel');
|
||||
|
||||
// Pencil = 3rd button (index 2): collapse, close, pencil, remove
|
||||
const allButtons = screen.getAllByRole('button');
|
||||
await userEvent.click(allButtons[2]);
|
||||
|
||||
// Extend end picker to Day 16 (id=7)
|
||||
await userEvent.click(getDayPickerTriggers()[1]);
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
// Old code: 17>=17 && 17<=7 → false (hotel vanishes). New code: position 0 in [0,15] → visible.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Span Hotel')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-061: non-monotonic IDs — hotel appears after create-save on intermediate day', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
const place = buildPlace({ id: 55, name: 'Created Hotel' });
|
||||
// Current day: days[5] = id 22, position 5 (within any full-span range)
|
||||
const currentDay = days[5];
|
||||
server.use(
|
||||
http.post('/api/trips/1/accommodations', async ({ request }) => {
|
||||
const body = await request.json() as any;
|
||||
return HttpResponse.json({
|
||||
accommodation: { id: 200, place_id: 55, place_name: 'Created Hotel', place_address: null,
|
||||
start_day_id: body.start_day_id, end_day_id: body.end_day_id,
|
||||
check_in: null, check_out: null, confirmation: null },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DayDetailPanel {...defaultProps} day={currentDay} days={days} places={[place]} />);
|
||||
await userEvent.click(await screen.findByText(/Add accommodation/i));
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Created Hotel/i }));
|
||||
|
||||
// Extend end to Day 16 (id=7) — start stays at current day id=22
|
||||
await userEvent.click(getDayPickerTriggers()[1]);
|
||||
await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||
|
||||
// Old code: 22>=22 && 22<=7 → false (hotel vanishes). New code: position 5 in [5,15] → visible.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Created Hotel')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-062: non-monotonic IDs — hotel shown on initial load when it spans the full trip', async () => {
|
||||
const days = buildNonMonotonicDays();
|
||||
server.use(
|
||||
http.get('/api/trips/1/accommodations', () =>
|
||||
HttpResponse.json({
|
||||
accommodations: [{ id: 1, place_id: 60, place_name: 'Full Trip Hotel', place_address: null,
|
||||
start_day_id: 17, end_day_id: 7, check_in: null, check_out: null, confirmation: null }],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// Day 1 (id=17): old filter: 17>=17 && 17<=7 → false. New: position 0 in [0,15] → visible.
|
||||
render(<DayDetailPanel {...defaultProps} day={days[0]} days={days} />);
|
||||
await screen.findByText('Full Trip Hotel');
|
||||
|
||||
// Intermediate day (id=1, position 9): old filter: 1>=17 → false. New: 9 in [0,15] → visible.
|
||||
render(<DayDetailPanel {...defaultProps} day={days[9]} days={days} />);
|
||||
await screen.findByText('Full Trip Hotel');
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-040: 12h time format renders reservation time with AM/PM', async () => {
|
||||
seedStore(useSettingsStore, {
|
||||
settings: { time_format: '12h', temperature_unit: 'celsius', blur_booking_codes: false },
|
||||
|
||||
@@ -12,6 +12,7 @@ import CustomTimePicker from '../shared/CustomTimePicker'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { getLocaleForLanguage, useTranslation } from '../../i18n'
|
||||
import type { Day, Place, Category, Reservation, AssignmentsMap } from '../../types'
|
||||
import { isDayInAccommodationRange } from '../../utils/dayOrder'
|
||||
|
||||
const WEATHER_ICON_MAP = {
|
||||
Clear: Sun, Clouds: Cloud, Rain: CloudRain, Drizzle: CloudDrizzle,
|
||||
@@ -99,7 +100,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
.then(data => {
|
||||
setAccommodations(data.accommodations || [])
|
||||
const allForDay = (data.accommodations || []).filter(a =>
|
||||
days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id)
|
||||
day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false
|
||||
)
|
||||
setDayAccommodations(allForDay)
|
||||
setAccommodation(allForDay[0] || null)
|
||||
@@ -130,7 +131,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
setAccommodations(updated)
|
||||
setAccommodation(newAcc)
|
||||
setDayAccommodations(updated.filter(a =>
|
||||
days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id)
|
||||
day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false
|
||||
))
|
||||
setShowHotelPicker(false)
|
||||
setHotelForm({ check_in: '', check_in_end: '', check_out: '', confirmation: '', place_id: null })
|
||||
@@ -154,7 +155,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
const updated = accommodations.filter(a => a.id !== accommodation.id)
|
||||
setAccommodations(updated)
|
||||
setDayAccommodations(updated.filter(a =>
|
||||
days.some(d => d.id >= a.start_day_id && d.id <= a.end_day_id && d.id === day?.id)
|
||||
day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false
|
||||
))
|
||||
setAccommodation(null)
|
||||
onAccommodationChange?.()
|
||||
@@ -463,7 +464,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<CustomSelect
|
||||
value={hotelDayRange.start}
|
||||
onChange={v => setHotelDayRange(prev => ({ start: v, end: Math.max(v, prev.end) }))}
|
||||
onChange={v => setHotelDayRange(prev => ({ start: v, end: days.findIndex(d => d.id === v) > days.findIndex(d => d.id === prev.end) ? v : prev.end }))}
|
||||
options={days.map((d, i) => ({
|
||||
value: d.id,
|
||||
label: d.title || t('planner.dayN', { n: i + 1 }),
|
||||
@@ -478,7 +479,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<CustomSelect
|
||||
value={hotelDayRange.end}
|
||||
onChange={v => setHotelDayRange(prev => ({ start: Math.min(prev.start, v), end: v }))}
|
||||
onChange={v => setHotelDayRange(prev => ({ start: days.findIndex(d => d.id === v) < days.findIndex(d => d.id === prev.start) ? v : prev.start, end: v }))}
|
||||
options={days.map((d, i) => ({
|
||||
value: d.id,
|
||||
label: d.title || t('planner.dayN', { n: i + 1 }),
|
||||
@@ -598,9 +599,9 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
const all = d.accommodations || []
|
||||
setAccommodations(all)
|
||||
setDayAccommodations(all.filter(a =>
|
||||
days.some(dd => dd.id >= a.start_day_id && dd.id <= a.end_day_id && dd.id === day?.id)
|
||||
day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false
|
||||
))
|
||||
const acc = all.find(a => days.some(dd => dd.id >= a.start_day_id && dd.id <= a.end_day_id && dd.id === day?.id))
|
||||
const acc = all.find(a => day ? isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days) : false)
|
||||
setAccommodation(acc || null)
|
||||
})
|
||||
onAccommodationChange?.()
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useTripStore } from '../../store/tripStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { isDayInAccommodationRange } from '../../utils/dayOrder'
|
||||
import { formatDate, formatTime, dayTotalCost, currencyDecimals } from '../../utils/formatters'
|
||||
import { useDayNotes } from '../../hooks/useDayNotes'
|
||||
import Tooltip from '../shared/Tooltip'
|
||||
@@ -397,7 +398,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
const getTransportForDay = (dayId: number) => {
|
||||
const dayAssignmentIds = (assignments[String(dayId)] || []).map(a => a.id)
|
||||
return reservations.filter(r => {
|
||||
if (r.type === 'hotel') return false
|
||||
if (!TRANSPORT_TYPES.has(r.type)) return false
|
||||
if (r.assignment_id && dayAssignmentIds.includes(r.assignment_id)) return false
|
||||
|
||||
const startDayId = r.day_id
|
||||
@@ -1214,7 +1215,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
</Tooltip>
|
||||
)}
|
||||
{(() => {
|
||||
const dayAccs = accommodations.filter(a => day.id >= a.start_day_id && day.id <= a.end_day_id)
|
||||
const dayAccs = accommodations.filter(a => isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days))
|
||||
// Sort: check-out first, then ongoing stays, then check-in last
|
||||
.sort((a, b) => {
|
||||
const aIsOut = a.end_day_id === day.id && a.start_day_id !== day.id
|
||||
@@ -1725,7 +1726,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
||||
return (
|
||||
<React.Fragment key={`transport-${res.id}-${day.id}`}>
|
||||
<div
|
||||
onClick={() => canEditDays && onEditTransport?.(res)}
|
||||
onClick={() => {
|
||||
if (!canEditDays) return
|
||||
if (TRANSPORT_TYPES.has(res.type)) onEditTransport?.(res)
|
||||
else onEditReservation?.(res)
|
||||
}}
|
||||
onDragOver={e => {
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
// FE-PLANNER-TRANSMODAL-001 to FE-PLANNER-TRANSMODAL-021
|
||||
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useTripStore } from '../../store/tripStore';
|
||||
import { useAddonStore } from '../../store/addonStore';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import {
|
||||
buildUser,
|
||||
buildTrip,
|
||||
buildDay,
|
||||
buildReservation,
|
||||
buildTripFile,
|
||||
} from '../../../tests/helpers/factories';
|
||||
import { TransportModal } from './TransportModal';
|
||||
|
||||
vi.mock('react-router-dom', async (importActual) => {
|
||||
const actual = await importActual<typeof import('react-router-dom')>();
|
||||
return { ...actual, useParams: () => ({ id: '1' }) };
|
||||
});
|
||||
|
||||
vi.mock('../shared/CustomTimePicker', () => ({
|
||||
default: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
|
||||
<input data-testid="time-picker" type="text" value={value} onChange={e => onChange(e.target.value)} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./AirportSelect', () => ({
|
||||
default: ({ onChange }: { onChange: (a: any) => void }) => (
|
||||
<input data-testid="airport-select" type="text" onChange={e => onChange({ iata: e.target.value, name: e.target.value, city: '', country: '', lat: 0, lng: 0, tz: 'UTC', icao: null })} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./LocationSelect', () => ({
|
||||
default: ({ onChange }: { onChange: (l: any) => void }) => (
|
||||
<input data-testid="location-select" type="text" onChange={e => onChange({ name: e.target.value, lat: 0, lng: 0, address: null })} />
|
||||
),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
onSave: vi.fn().mockResolvedValue(undefined),
|
||||
reservation: null,
|
||||
days: [],
|
||||
selectedDayId: null,
|
||||
files: [],
|
||||
onFileUpload: vi.fn().mockResolvedValue(undefined),
|
||||
onFileDelete: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1 }), budgetItems: [] });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('TransportModal', () => {
|
||||
// ── Rendering ──────────────────────────────────────────────────────────────
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-001: renders without crashing', () => {
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
expect(document.body).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-002: shows "Add transport" title for new transport', () => {
|
||||
render(<TransportModal {...defaultProps} reservation={null} />);
|
||||
expect(screen.getByText(/Add transport/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-003: shows "Edit transport" title when editing', () => {
|
||||
const res = buildReservation({ title: 'Paris Flight', type: 'flight' });
|
||||
render(<TransportModal {...defaultProps} reservation={res} />);
|
||||
expect(screen.getByText(/Edit transport/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-004: title input is required — onSave not called with empty title', async () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(<TransportModal {...defaultProps} onSave={onSave} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-005: all 4 transport type buttons are visible', () => {
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: /^Flight$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^Train$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^Car$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^Cruise$/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-006: editing pre-fills title', () => {
|
||||
const res = buildReservation({ title: 'LH123 Frankfurt', type: 'flight' });
|
||||
render(<TransportModal {...defaultProps} reservation={res} />);
|
||||
expect(screen.getByDisplayValue('LH123 Frankfurt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-007: edit mode save button shows "Update"', () => {
|
||||
const res = buildReservation({ title: 'My Train', type: 'train' });
|
||||
render(<TransportModal {...defaultProps} reservation={res} />);
|
||||
expect(screen.getByRole('button', { name: /^Update$/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-008: Cancel button calls onClose', async () => {
|
||||
const onClose = vi.fn();
|
||||
render(<TransportModal {...defaultProps} onClose={onClose} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-009: submitting valid flight calls onSave with correct type', async () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(<TransportModal {...defaultProps} onSave={onSave} />);
|
||||
await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'LH456');
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalled());
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ title: 'LH456', type: 'flight' }));
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-010: switching to train type calls onSave with train type', async () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(<TransportModal {...defaultProps} onSave={onSave} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Train$/i }));
|
||||
await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Eurostar');
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalled());
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ type: 'train' }));
|
||||
});
|
||||
|
||||
// ── Budget addon ─────────────────────────────────────────────────────────────
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-011: budget section visible when addon is enabled', () => {
|
||||
seedStore(useAddonStore, {
|
||||
addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
|
||||
loaded: true,
|
||||
});
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
expect(screen.getByText(/^Price$/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Budget category/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-012: budget section not shown when addon is disabled', () => {
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
expect(screen.queryByPlaceholderText('0.00')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-013: budget fields included in onSave when price is set', async () => {
|
||||
seedStore(useAddonStore, {
|
||||
addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
|
||||
loaded: true,
|
||||
});
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
render(<TransportModal {...defaultProps} onSave={onSave} />);
|
||||
await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'ICE Train');
|
||||
await userEvent.type(screen.getByPlaceholderText('0.00'), '85');
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalled());
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ create_budget_entry: expect.objectContaining({ total_price: 85 }) })
|
||||
);
|
||||
});
|
||||
|
||||
// ── File attachment ───────────────────────────────────────────────────────────
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-014: attach file button rendered when onFileUpload provided', () => {
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: /Attach file/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-015: attach file button absent when onFileUpload is undefined', () => {
|
||||
render(<TransportModal {...defaultProps} onFileUpload={undefined} />);
|
||||
expect(screen.queryByRole('button', { name: /Attach file/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-016: attached files shown for existing transport', () => {
|
||||
const res = buildReservation({ id: 5, type: 'flight' });
|
||||
const file = buildTripFile({ id: 1, trip_id: 1, original_name: 'boarding-pass.pdf' });
|
||||
(file as any).reservation_id = 5;
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} files={[file]} />);
|
||||
expect(screen.getByText('boarding-pass.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-017: pending file added for new transport on file input change', async () => {
|
||||
render(<TransportModal {...defaultProps} reservation={null} />);
|
||||
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const testFile = new File(['content'], 'itinerary.pdf', { type: 'application/pdf' });
|
||||
fireEvent.change(fileInput, { target: { files: [testFile] } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('itinerary.pdf')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-018: file upload to existing transport calls onFileUpload with correct FormData', async () => {
|
||||
const onFileUpload = vi.fn().mockResolvedValue(undefined);
|
||||
const res = buildReservation({ id: 10, type: 'train', title: 'Eurostar' });
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} onFileUpload={onFileUpload} />);
|
||||
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const testFile = new File(['content'], 'ticket.pdf', { type: 'application/pdf' });
|
||||
fireEvent.change(fileInput, { target: { files: [testFile] } });
|
||||
|
||||
await waitFor(() => expect(onFileUpload).toHaveBeenCalled());
|
||||
const [fd] = onFileUpload.mock.calls[0] as [FormData];
|
||||
expect(fd.get('file')).toBeTruthy();
|
||||
expect(fd.get('reservation_id')).toBe('10');
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-019: link existing file button appears when unattached files exist', () => {
|
||||
const res = buildReservation({ id: 5, type: 'flight' });
|
||||
const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} files={[unattachedFile]} />);
|
||||
expect(screen.getByRole('button', { name: /Link existing file/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-020: clicking "link existing file" shows file picker dropdown', async () => {
|
||||
const res = buildReservation({ id: 5, type: 'flight' });
|
||||
const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} files={[unattachedFile]} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
|
||||
expect(screen.getByText('invoice.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-021: clicking file in picker links it and closes picker', async () => {
|
||||
server.use(
|
||||
http.post('/api/trips/1/files/99/link', () => HttpResponse.json({ success: true })),
|
||||
http.get('/api/trips/1/files', () => HttpResponse.json({ files: [] })),
|
||||
);
|
||||
|
||||
const res = buildReservation({ id: 5, type: 'flight' });
|
||||
const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} files={[unattachedFile]} />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
|
||||
await userEvent.click(screen.getByText('invoice.pdf'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: /Link existing file/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-022: removing pending file removes it from list', async () => {
|
||||
render(<TransportModal {...defaultProps} reservation={null} />);
|
||||
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const testFile = new File(['content'], 'draft.pdf', { type: 'application/pdf' });
|
||||
fireEvent.change(fileInput, { target: { files: [testFile] } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('draft.pdf')).toBeInTheDocument());
|
||||
|
||||
const pendingFileRow = screen.getByText('draft.pdf').closest('div')!;
|
||||
const removeBtn = pendingFileRow.querySelector('button')!;
|
||||
await userEvent.click(removeBtn);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('draft.pdf')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-023: clicking attach file button triggers file input click', async () => {
|
||||
render(<TransportModal {...defaultProps} />);
|
||||
const attachBtn = screen.getByRole('button', { name: /Attach file/i });
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const clickSpy = vi.spyOn(fileInput, 'click').mockImplementation(() => {});
|
||||
await userEvent.click(attachBtn);
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
clickSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-024: unlinking a linked file removes it from attached list', async () => {
|
||||
server.use(
|
||||
http.post('/api/trips/1/files/42/link', () => HttpResponse.json({ success: true })),
|
||||
http.get('/api/trips/1/files/42/links', () => HttpResponse.json({ links: [{ id: 1, reservation_id: 7 }] })),
|
||||
http.delete('/api/trips/1/files/42/link/1', () => HttpResponse.json({ success: true })),
|
||||
http.get('/api/trips/1/files', () => HttpResponse.json({ files: [] })),
|
||||
);
|
||||
|
||||
const res = buildReservation({ id: 7, type: 'car' });
|
||||
const looseFile = buildTripFile({ id: 42, original_name: 'rental-agreement.pdf' });
|
||||
|
||||
render(<TransportModal {...defaultProps} reservation={res} files={[looseFile]} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
|
||||
await waitFor(() => expect(screen.getByText('rental-agreement.pdf')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('rental-agreement.pdf'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('button', { name: /Link existing file/i })).not.toBeInTheDocument()
|
||||
);
|
||||
|
||||
const fileRow = screen.getByText('rental-agreement.pdf').closest('div')!;
|
||||
const unlinkBtn = fileRow.querySelector('button[type="button"]')!;
|
||||
await userEvent.click(unlinkBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /Link existing file/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-PLANNER-TRANSMODAL-025: pending files flushed after saving new transport', async () => {
|
||||
const savedReservation = buildReservation({ id: 99, type: 'flight' });
|
||||
const onSave = vi.fn().mockResolvedValue(savedReservation);
|
||||
const onFileUpload = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(<TransportModal {...defaultProps} onSave={onSave} onFileUpload={onFileUpload} reservation={null} />);
|
||||
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const testFile = new File(['content'], 'boarding.pdf', { type: 'application/pdf' });
|
||||
fireEvent.change(fileInput, { target: { files: [testFile] } });
|
||||
await waitFor(() => expect(screen.getByText('boarding.pdf')).toBeInTheDocument());
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'LH001');
|
||||
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
|
||||
await waitFor(() => expect(onFileUpload).toHaveBeenCalled());
|
||||
const [fd] = onFileUpload.mock.calls[0] as [FormData];
|
||||
expect(fd.get('reservation_id')).toBe('99');
|
||||
expect(fd.get('file')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Plane, Train, Car, Ship } from 'lucide-react'
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Plane, Train, Car, Ship, Paperclip, FileText, X, ExternalLink, Link2 } from 'lucide-react'
|
||||
import Modal from '../shared/Modal'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import CustomTimePicker from '../shared/CustomTimePicker'
|
||||
@@ -10,7 +11,9 @@ import { useToast } from '../shared/Toast'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import { formatDate } from '../../utils/formatters'
|
||||
import type { Day, Reservation, ReservationEndpoint } from '../../types'
|
||||
import { openFile } from '../../utils/fileDownload'
|
||||
import apiClient from '../../api/client'
|
||||
import type { Day, Reservation, ReservationEndpoint, TripFile } from '../../types'
|
||||
|
||||
const TRANSPORT_TYPES = ['flight', 'train', 'car', 'cruise'] as const
|
||||
type TransportType = typeof TRANSPORT_TYPES[number]
|
||||
@@ -89,26 +92,36 @@ const defaultForm = {
|
||||
interface TransportModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (data: Record<string, any>) => Promise<void>
|
||||
onSave: (data: Record<string, any>) => Promise<Reservation | undefined>
|
||||
reservation: Reservation | null
|
||||
days: Day[]
|
||||
selectedDayId: number | null
|
||||
files?: TripFile[]
|
||||
onFileUpload?: (fd: FormData) => Promise<void>
|
||||
onFileDelete?: (fileId: number) => Promise<void>
|
||||
}
|
||||
|
||||
export function TransportModal({ isOpen, onClose, onSave, reservation, days, selectedDayId }: TransportModalProps) {
|
||||
export function TransportModal({ isOpen, onClose, onSave, reservation, days, selectedDayId, files = [], onFileUpload, onFileDelete }: TransportModalProps) {
|
||||
const { t, locale } = useTranslation()
|
||||
const toast = useToast()
|
||||
const isBudgetEnabled = useAddonStore(s => s.isEnabled('budget'))
|
||||
const budgetItems = useTripStore(s => s.budgetItems)
|
||||
const loadFiles = useTripStore(s => s.loadFiles)
|
||||
const budgetCategories = useMemo(() => {
|
||||
const cats = new Set<string>()
|
||||
budgetItems.forEach(i => { if (i.category) cats.add(i.category) })
|
||||
return Array.from(cats).sort()
|
||||
}, [budgetItems])
|
||||
const { id: tripId } = useParams<{ id: string }>()
|
||||
const [form, setForm] = useState({ ...defaultForm })
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [fromPick, setFromPick] = useState<EndpointPick>({})
|
||||
const [toPick, setToPick] = useState<EndpointPick>({})
|
||||
const [uploadingFile, setUploadingFile] = useState(false)
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([])
|
||||
const [showFilePicker, setShowFilePicker] = useState(false)
|
||||
const [linkedFileIds, setLinkedFileIds] = useState<number[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@@ -222,7 +235,16 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
||||
? { total_price: parseFloat(form.price), category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other' }
|
||||
: { total_price: 0 }
|
||||
}
|
||||
await onSave(payload)
|
||||
const saved = await onSave(payload)
|
||||
if (!reservation?.id && saved?.id && pendingFiles.length > 0 && onFileUpload) {
|
||||
for (const file of pendingFiles) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('reservation_id', String(saved.id))
|
||||
fd.append('description', form.title)
|
||||
await onFileUpload(fd)
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
|
||||
} finally {
|
||||
@@ -230,6 +252,38 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
if (reservation?.id) {
|
||||
setUploadingFile(true)
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('reservation_id', String(reservation.id))
|
||||
fd.append('description', reservation.title)
|
||||
await onFileUpload!(fd)
|
||||
toast.success(t('reservations.toast.fileUploaded'))
|
||||
} catch {
|
||||
toast.error(t('reservations.toast.uploadError'))
|
||||
} finally {
|
||||
setUploadingFile(false)
|
||||
e.target.value = ''
|
||||
}
|
||||
} else {
|
||||
setPendingFiles(prev => [...prev, file])
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const attachedFiles = reservation?.id
|
||||
? files.filter(f =>
|
||||
f.reservation_id === reservation.id ||
|
||||
linkedFileIds.includes(f.id) ||
|
||||
(f.linked_reservation_ids && f.linked_reservation_ids.includes(reservation.id))
|
||||
)
|
||||
: []
|
||||
|
||||
const inputStyle = {
|
||||
width: '100%', border: '1px solid var(--border-primary)', borderRadius: 10,
|
||||
padding: '8px 12px', fontSize: 13, fontFamily: 'inherit',
|
||||
@@ -444,6 +498,94 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
||||
style={{ ...inputStyle, resize: 'none', lineHeight: 1.5 }} />
|
||||
</div>
|
||||
|
||||
{/* Files */}
|
||||
<div>
|
||||
<label style={labelStyle}>{t('files.title')}</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{attachedFiles.map(f => (
|
||||
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}>
|
||||
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); openFile(f.url).catch(() => {}) }} style={{ color: 'var(--text-faint)', display: 'flex', flexShrink: 0, cursor: 'pointer' }}><ExternalLink size={11} /></a>
|
||||
<button type="button" onClick={async () => {
|
||||
if (f.reservation_id === reservation?.id) {
|
||||
try { await apiClient.put(`/trips/${tripId}/files/${f.id}`, { reservation_id: null }) } catch {}
|
||||
}
|
||||
try {
|
||||
const linksRes = await apiClient.get(`/trips/${tripId}/files/${f.id}/links`)
|
||||
const link = (linksRes.data.links || []).find((l: any) => l.reservation_id === reservation?.id)
|
||||
if (link) await apiClient.delete(`/trips/${tripId}/files/${f.id}/link/${link.id}`)
|
||||
} catch {}
|
||||
setLinkedFileIds(prev => prev.filter(id => id !== f.id))
|
||||
if (tripId) loadFiles(tripId)
|
||||
}} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}>
|
||||
<X size={11} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{pendingFiles.map((f, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', background: 'var(--bg-secondary)', borderRadius: 8 }}>
|
||||
<FileText size={12} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
|
||||
<button type="button" onClick={() => setPendingFiles(prev => prev.filter((_, j) => j !== i))}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-faint)', display: 'flex', padding: 0, flexShrink: 0 }}>
|
||||
<X size={11} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.txt,image/*" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{onFileUpload && <button type="button" onClick={() => fileInputRef.current?.click()} disabled={uploadingFile} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
|
||||
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
|
||||
fontSize: 11, color: 'var(--text-faint)', cursor: uploadingFile ? 'default' : 'pointer', fontFamily: 'inherit',
|
||||
}}>
|
||||
<Paperclip size={11} />
|
||||
{uploadingFile ? t('reservations.uploading') : t('reservations.attachFile')}
|
||||
</button>}
|
||||
{reservation?.id && files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).length > 0 && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button type="button" onClick={() => setShowFilePicker(v => !v)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5, padding: '6px 10px',
|
||||
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
|
||||
fontSize: 11, color: 'var(--text-faint)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>
|
||||
<Link2 size={11} /> {t('reservations.linkExisting')}
|
||||
</button>
|
||||
{showFilePicker && (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: '100%', left: 0, marginBottom: 4, zIndex: 50,
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 220, maxHeight: 200, overflowY: 'auto',
|
||||
}}>
|
||||
{files.filter(f => !f.deleted_at && !attachedFiles.some(af => af.id === f.id)).map(f => (
|
||||
<button key={f.id} type="button" onClick={async () => {
|
||||
try {
|
||||
await apiClient.post(`/trips/${tripId}/files/${f.id}/link`, { reservation_id: reservation.id })
|
||||
setLinkedFileIds(prev => [...prev, f.id])
|
||||
setShowFilePicker(false)
|
||||
if (tripId) loadFiles(tripId)
|
||||
} catch {}
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 10px',
|
||||
background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit',
|
||||
color: 'var(--text-secondary)', borderRadius: 7, textAlign: 'left',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'none'}>
|
||||
<FileText size={12} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price + Budget Category */}
|
||||
{isBudgetEnabled && (
|
||||
<>
|
||||
|
||||
@@ -1249,6 +1249,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'فشل حذف الملف',
|
||||
'files.sourcePlan': 'خطة اليوم',
|
||||
'files.sourceBooking': 'الحجز',
|
||||
'files.sourceTransport': 'النقل',
|
||||
'files.attach': 'إرفاق',
|
||||
'files.pasteHint': 'يمكنك أيضًا لصق الصور من الحافظة (Ctrl+V)',
|
||||
'files.trash': 'سلة المهملات',
|
||||
@@ -1261,6 +1262,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'إسناد ملف',
|
||||
'files.assignPlace': 'المكان',
|
||||
'files.assignBooking': 'الحجز',
|
||||
'files.assignTransport': 'النقل',
|
||||
'files.unassigned': 'غير مسند',
|
||||
'files.unlink': 'إزالة الرابط',
|
||||
'files.toast.trashed': 'تم النقل إلى سلة المهملات',
|
||||
|
||||
@@ -1218,6 +1218,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Falha ao excluir arquivo',
|
||||
'files.sourcePlan': 'Plano do dia',
|
||||
'files.sourceBooking': 'Reserva',
|
||||
'files.sourceTransport': 'Transporte',
|
||||
'files.attach': 'Anexar',
|
||||
'files.pasteHint': 'Você também pode colar imagens da área de transferência (Ctrl+V)',
|
||||
'files.trash': 'Lixeira',
|
||||
@@ -1230,6 +1231,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Atribuir arquivo',
|
||||
'files.assignPlace': 'Lugar',
|
||||
'files.assignBooking': 'Reserva',
|
||||
'files.assignTransport': 'Transporte',
|
||||
'files.unassigned': 'Não atribuído',
|
||||
'files.unlink': 'Remover vínculo',
|
||||
'files.toast.trashed': 'Movido para a lixeira',
|
||||
|
||||
@@ -1247,6 +1247,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Nepodařilo se smazat soubor',
|
||||
'files.sourcePlan': 'Denní plán',
|
||||
'files.sourceBooking': 'Rezervace',
|
||||
'files.sourceTransport': 'Doprava',
|
||||
'files.attach': 'Přiložit',
|
||||
'files.pasteHint': 'Můžete také vložit obrázek ze schránky (Ctrl+V)',
|
||||
'files.trash': 'Koš',
|
||||
@@ -1259,6 +1260,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Přiřadit soubor',
|
||||
'files.assignPlace': 'Místo',
|
||||
'files.assignBooking': 'Rezervace',
|
||||
'files.assignTransport': 'Doprava',
|
||||
'files.unassigned': 'Nepřiřazeno',
|
||||
'files.unlink': 'Zrušit propojení',
|
||||
'files.toast.trashed': 'Přesunuto do koše',
|
||||
|
||||
@@ -1251,6 +1251,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Fehler beim Löschen der Datei',
|
||||
'files.sourcePlan': 'Tagesplan',
|
||||
'files.sourceBooking': 'Buchung',
|
||||
'files.sourceTransport': 'Transport',
|
||||
'files.attach': 'Anhängen',
|
||||
'files.pasteHint': 'Du kannst auch Bilder aus der Zwischenablage einfügen (Strg+V)',
|
||||
'files.trash': 'Papierkorb',
|
||||
@@ -1263,6 +1264,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Datei zuweisen',
|
||||
'files.assignPlace': 'Ort',
|
||||
'files.assignBooking': 'Buchung',
|
||||
'files.assignTransport': 'Transport',
|
||||
'files.unassigned': 'Nicht zugewiesen',
|
||||
'files.unlink': 'Verknüpfung entfernen',
|
||||
'files.toast.trashed': 'In den Papierkorb verschoben',
|
||||
|
||||
@@ -1322,6 +1322,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Failed to delete file',
|
||||
'files.sourcePlan': 'Day Plan',
|
||||
'files.sourceBooking': 'Booking',
|
||||
'files.sourceTransport': 'Transport',
|
||||
'files.attach': 'Attach',
|
||||
'files.pasteHint': 'You can also paste images from clipboard (Ctrl+V)',
|
||||
'files.trash': 'Trash',
|
||||
@@ -1334,6 +1335,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Assign File',
|
||||
'files.assignPlace': 'Place',
|
||||
'files.assignBooking': 'Booking',
|
||||
'files.assignTransport': 'Transport',
|
||||
'files.unassigned': 'Unassigned',
|
||||
'files.unlink': 'Remove link',
|
||||
'files.toast.trashed': 'Moved to trash',
|
||||
|
||||
@@ -1195,6 +1195,7 @@ const es: Record<string, string> = {
|
||||
'files.toast.deleteError': 'No se pudo eliminar el archivo',
|
||||
'files.sourcePlan': 'Plan diario',
|
||||
'files.sourceBooking': 'Reserva',
|
||||
'files.sourceTransport': 'Transporte',
|
||||
'files.attach': 'Adjuntar',
|
||||
'files.pasteHint': 'También puedes pegar imágenes desde el portapapeles (Ctrl+V)',
|
||||
|
||||
@@ -1682,6 +1683,7 @@ const es: Record<string, string> = {
|
||||
'files.assignTitle': 'Asignar archivo',
|
||||
'files.assignPlace': 'Lugar',
|
||||
'files.assignBooking': 'Reserva',
|
||||
'files.assignTransport': 'Transporte',
|
||||
'files.unassigned': 'Sin asignar',
|
||||
'files.unlink': 'Eliminar vínculo',
|
||||
'files.noteLabel': 'Nota',
|
||||
|
||||
@@ -1245,6 +1245,7 @@ const fr: Record<string, string> = {
|
||||
'files.toast.deleteError': 'Impossible de supprimer le fichier',
|
||||
'files.sourcePlan': 'Plan du jour',
|
||||
'files.sourceBooking': 'Réservation',
|
||||
'files.sourceTransport': 'Transport',
|
||||
'files.attach': 'Joindre',
|
||||
'files.pasteHint': 'Vous pouvez aussi coller des images depuis le presse-papiers (Ctrl+V)',
|
||||
'files.trash': 'Corbeille',
|
||||
@@ -1257,6 +1258,7 @@ const fr: Record<string, string> = {
|
||||
'files.assignTitle': 'Assigner le fichier',
|
||||
'files.assignPlace': 'Lieu',
|
||||
'files.assignBooking': 'Réservation',
|
||||
'files.assignTransport': 'Transport',
|
||||
'files.unassigned': 'Non attribué',
|
||||
'files.unlink': 'Supprimer le lien',
|
||||
'files.toast.trashed': 'Déplacé dans la corbeille',
|
||||
|
||||
@@ -1246,6 +1246,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Nem sikerült törölni a fájlt',
|
||||
'files.sourcePlan': 'Napi terv',
|
||||
'files.sourceBooking': 'Foglalás',
|
||||
'files.sourceTransport': 'Közlekedés',
|
||||
'files.attach': 'Csatolás',
|
||||
'files.pasteHint': 'Képeket a vágólapról is beillesztheted (Ctrl+V)',
|
||||
'files.trash': 'Kuka',
|
||||
@@ -1258,6 +1259,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Fájl hozzárendelése',
|
||||
'files.assignPlace': 'Hely',
|
||||
'files.assignBooking': 'Foglalás',
|
||||
'files.assignTransport': 'Közlekedés',
|
||||
'files.unassigned': 'Nincs hozzárendelve',
|
||||
'files.unlink': 'Kapcsolat eltávolítása',
|
||||
'files.toast.trashed': 'Kukába helyezve',
|
||||
|
||||
@@ -1306,6 +1306,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Gagal menghapus file',
|
||||
'files.sourcePlan': 'Rencana Harian',
|
||||
'files.sourceBooking': 'Pemesanan',
|
||||
'files.sourceTransport': 'Transportasi',
|
||||
'files.attach': 'Lampirkan',
|
||||
'files.pasteHint': 'Kamu juga bisa menempel gambar dari clipboard (Ctrl+V)',
|
||||
'files.trash': 'Sampah',
|
||||
@@ -1318,6 +1319,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Tugaskan File',
|
||||
'files.assignPlace': 'Tempat',
|
||||
'files.assignBooking': 'Pemesanan',
|
||||
'files.assignTransport': 'Transportasi',
|
||||
'files.unassigned': 'Tidak ditugaskan',
|
||||
'files.unlink': 'Hapus tautan',
|
||||
'files.toast.trashed': 'Dipindahkan ke sampah',
|
||||
|
||||
@@ -1246,6 +1246,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Impossibile eliminare il file',
|
||||
'files.sourcePlan': 'Programma giornaliero',
|
||||
'files.sourceBooking': 'Prenotazione',
|
||||
'files.sourceTransport': 'Trasporto',
|
||||
'files.attach': 'Allega',
|
||||
'files.pasteHint': 'Puoi anche incollare immagini dagli appunti (Ctrl+V)',
|
||||
'files.trash': 'Cestino',
|
||||
@@ -1258,6 +1259,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Assegna file',
|
||||
'files.assignPlace': 'Luogo',
|
||||
'files.assignBooking': 'Prenotazione',
|
||||
'files.assignTransport': 'Trasporto',
|
||||
'files.unassigned': 'Non assegnato',
|
||||
'files.unlink': 'Rimuovi collegamento',
|
||||
'files.toast.trashed': 'Spostato nel cestino',
|
||||
|
||||
@@ -1245,6 +1245,7 @@ const nl: Record<string, string> = {
|
||||
'files.toast.deleteError': 'Bestand verwijderen mislukt',
|
||||
'files.sourcePlan': 'Dagplan',
|
||||
'files.sourceBooking': 'Boeking',
|
||||
'files.sourceTransport': 'Transport',
|
||||
'files.attach': 'Bijvoegen',
|
||||
'files.pasteHint': 'Je kunt ook afbeeldingen plakken vanuit het klembord (Ctrl+V)',
|
||||
'files.trash': 'Prullenbak',
|
||||
@@ -1257,6 +1258,7 @@ const nl: Record<string, string> = {
|
||||
'files.assignTitle': 'Bestand toewijzen',
|
||||
'files.assignPlace': 'Plaats',
|
||||
'files.assignBooking': 'Boeking',
|
||||
'files.assignTransport': 'Transport',
|
||||
'files.unassigned': 'Niet toegewezen',
|
||||
'files.unlink': 'Koppeling verwijderen',
|
||||
'files.toast.trashed': 'Naar prullenbak verplaatst',
|
||||
|
||||
@@ -1197,6 +1197,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.toast.deleteError': 'Nie udało się usunąć pliku',
|
||||
'files.sourcePlan': 'Plan dni',
|
||||
'files.sourceBooking': 'Rezerwacje',
|
||||
'files.sourceTransport': 'Transport',
|
||||
'files.attach': 'Załącz',
|
||||
'files.pasteHint': 'Możesz również wkleić obrazki ze schowka (Ctrl+V)',
|
||||
'files.trash': 'Kosz',
|
||||
@@ -1209,6 +1210,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'files.assignTitle': 'Przypisz plik',
|
||||
'files.assignPlace': 'Miejsce',
|
||||
'files.assignBooking': 'Rezerwacja',
|
||||
'files.assignTransport': 'Transport',
|
||||
'files.unassigned': 'Nieprzypisane',
|
||||
'files.unlink': 'Usuń link',
|
||||
'files.toast.trashed': 'Przeniesiono do kosza',
|
||||
|
||||
@@ -1245,6 +1245,7 @@ const ru: Record<string, string> = {
|
||||
'files.toast.deleteError': 'Не удалось удалить файл',
|
||||
'files.sourcePlan': 'План дня',
|
||||
'files.sourceBooking': 'Бронирование',
|
||||
'files.sourceTransport': 'Транспорт',
|
||||
'files.attach': 'Прикрепить',
|
||||
'files.pasteHint': 'Также можно вставить изображения из буфера обмена (Ctrl+V)',
|
||||
'files.trash': 'Корзина',
|
||||
@@ -1257,6 +1258,7 @@ const ru: Record<string, string> = {
|
||||
'files.assignTitle': 'Назначить файл',
|
||||
'files.assignPlace': 'Место',
|
||||
'files.assignBooking': 'Бронирование',
|
||||
'files.assignTransport': 'Транспорт',
|
||||
'files.unassigned': 'Не назначен',
|
||||
'files.unlink': 'Удалить связь',
|
||||
'files.toast.trashed': 'Перемещено в корзину',
|
||||
|
||||
@@ -1245,6 +1245,7 @@ const zh: Record<string, string> = {
|
||||
'files.toast.deleteError': '删除文件失败',
|
||||
'files.sourcePlan': '日程计划',
|
||||
'files.sourceBooking': '预订',
|
||||
'files.sourceTransport': '交通',
|
||||
'files.attach': '附加',
|
||||
'files.pasteHint': '也可以从剪贴板粘贴图片 (Ctrl+V)',
|
||||
'files.trash': '回收站',
|
||||
@@ -1257,6 +1258,7 @@ const zh: Record<string, string> = {
|
||||
'files.assignTitle': '分配文件',
|
||||
'files.assignPlace': '地点',
|
||||
'files.assignBooking': '预订',
|
||||
'files.assignTransport': '交通',
|
||||
'files.unassigned': '未分配',
|
||||
'files.unlink': '移除关联',
|
||||
'files.toast.trashed': '已移至回收站',
|
||||
|
||||
@@ -1305,6 +1305,7 @@ const zhTw: Record<string, string> = {
|
||||
'files.toast.deleteError': '刪除檔案失敗',
|
||||
'files.sourcePlan': '日程計劃',
|
||||
'files.sourceBooking': '預訂',
|
||||
'files.sourceTransport': '交通',
|
||||
'files.attach': '附加',
|
||||
'files.pasteHint': '也可以從剪貼簿貼上圖片 (Ctrl+V)',
|
||||
'files.trash': '回收站',
|
||||
@@ -1317,6 +1318,7 @@ const zhTw: Record<string, string> = {
|
||||
'files.assignTitle': '分配檔案',
|
||||
'files.assignPlace': '地點',
|
||||
'files.assignBooking': '預訂',
|
||||
'files.assignTransport': '交通',
|
||||
'files.unassigned': '未分配',
|
||||
'files.unlink': '移除關聯',
|
||||
'files.toast.trashed': '已移至回收站',
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '../../tests/helpers/render';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../tests/helpers/msw/server';
|
||||
import { resetAllStores } from '../../tests/helpers/store';
|
||||
import LoginPage from './LoginPage';
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return { ...actual, useNavigate: () => mockNavigate };
|
||||
});
|
||||
|
||||
describe('LoginPage — OIDC redirect preservation', () => {
|
||||
let savedLocation: Location;
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
mockNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
savedLocation = window.location;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: savedLocation,
|
||||
});
|
||||
});
|
||||
|
||||
function setSearch(search: string) {
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: { ...window.location, search },
|
||||
});
|
||||
}
|
||||
|
||||
describe('FE-PAGE-LOGIN-022: redirect param stashed in sessionStorage on mount', () => {
|
||||
it('saves decoded redirect to sessionStorage when ?redirect= is present', async () => {
|
||||
setSearch('?redirect=%2Foauth%2Fauthorize%3Fclient_id%3Dfoo');
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem('oidc_redirect')).toBe('/oauth/authorize?client_id=foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not write to sessionStorage when no redirect param is present', async () => {
|
||||
render(<LoginPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(sessionStorage.getItem('oidc_redirect')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-023: OIDC code exchange navigates to sessionStorage redirect', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
http.get('/api/auth/oidc/exchange', () =>
|
||||
HttpResponse.json({ token: 'mock-oidc-token' })
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the saved sessionStorage redirect after successful OIDC exchange', async () => {
|
||||
sessionStorage.setItem('oidc_redirect', '/oauth/authorize?client_id=foo&state=xyz');
|
||||
setSearch('?oidc_code=testcode123');
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
'/oauth/authorize?client_id=foo&state=xyz',
|
||||
{ replace: true },
|
||||
);
|
||||
});
|
||||
|
||||
expect(sessionStorage.getItem('oidc_redirect')).toBeNull();
|
||||
});
|
||||
|
||||
it('falls back to /dashboard when no sessionStorage redirect is set', async () => {
|
||||
setSearch('?oidc_code=testcode123');
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-LOGIN-024: OIDC error clears sessionStorage redirect', () => {
|
||||
it('removes oidc_redirect from sessionStorage on OIDC error', async () => {
|
||||
sessionStorage.setItem('oidc_redirect', '/oauth/authorize?client_id=foo');
|
||||
setSearch('?oidc_error=token_failed');
|
||||
render(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.getItem('oidc_redirect')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,12 @@ export default function LoginPage(): React.ReactElement {
|
||||
return '/dashboard'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectTarget !== '/dashboard') {
|
||||
sessionStorage.setItem('oidc_redirect', redirectTarget)
|
||||
}
|
||||
}, [redirectTarget])
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
@@ -83,7 +89,9 @@ export default function LoginPage(): React.ReactElement {
|
||||
window.history.replaceState({}, '', '/login')
|
||||
if (data.token) {
|
||||
await loadUser()
|
||||
navigate('/dashboard', { replace: true })
|
||||
const savedRedirect = sessionStorage.getItem('oidc_redirect') || '/dashboard'
|
||||
sessionStorage.removeItem('oidc_redirect')
|
||||
navigate(savedRedirect, { replace: true })
|
||||
} else {
|
||||
setError(data.error || t('login.oidcFailed'))
|
||||
}
|
||||
@@ -104,6 +112,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
invalid_state: t('login.oidc.invalidState'),
|
||||
}
|
||||
setError(errorMessages[oidcError] || oidcError)
|
||||
sessionStorage.removeItem('oidc_redirect')
|
||||
window.history.replaceState({}, '', '/login')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function OAuthAuthorizePage(): React.ReactElement {
|
||||
}
|
||||
|
||||
function handleLoginRedirect() {
|
||||
const next = '/oauth/authorize?' + params.toString()
|
||||
const next = '/oauth/authorize?' + params.toString() + window.location.hash
|
||||
window.location.href = '/login?redirect=' + encodeURIComponent(next)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getCategoryIcon } from '../components/shared/categoryIcons'
|
||||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { Clock, MapPin, FileText, Train, Plane, Bus, Car, Ship, Ticket, Hotel, Map, Luggage, Wallet, MessageCircle } from 'lucide-react'
|
||||
import { isDayInAccommodationRange } from '../utils/dayOrder'
|
||||
|
||||
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'cruise'])
|
||||
const TRANSPORT_ICONS = { flight: Plane, train: Train, bus: Bus, car: Car, cruise: Ship }
|
||||
@@ -184,7 +185,7 @@ export default function SharedTripPage() {
|
||||
const da = assignments[String(day.id)] || []
|
||||
const notes = (dayNotes[String(day.id)] || [])
|
||||
const dayTransport = (reservations || []).filter((r: any) => TRANSPORT_TYPES.has(r.type) && r.reservation_time?.split('T')[0] === day.date)
|
||||
const dayAccs = (accommodations || []).filter((a: any) => day.id >= a.start_day_id && day.id <= a.end_day_id)
|
||||
const dayAccs = (accommodations || []).filter((a: any) => isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, sortedDays))
|
||||
|
||||
const merged = [
|
||||
...da.map((a: any) => ({ type: 'place', k: a.order_index, data: a })),
|
||||
|
||||
@@ -666,15 +666,20 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
const handleSaveTransport = async (data) => {
|
||||
try {
|
||||
if (editingTransport) {
|
||||
await tripActions.updateReservation(tripId, editingTransport.id, data)
|
||||
const r = await tripActions.updateReservation(tripId, editingTransport.id, data)
|
||||
toast.success(t('trip.toast.reservationUpdated'))
|
||||
setShowTransportModal(false)
|
||||
setEditingTransport(null)
|
||||
setTransportModalDayId(null)
|
||||
return r
|
||||
} else {
|
||||
await tripActions.addReservation(tripId, data)
|
||||
const r = await tripActions.addReservation(tripId, data)
|
||||
toast.success(t('trip.toast.reservationAdded'))
|
||||
setShowTransportModal(false)
|
||||
setEditingTransport(null)
|
||||
setTransportModalDayId(null)
|
||||
return r
|
||||
}
|
||||
setShowTransportModal(false)
|
||||
setEditingTransport(null)
|
||||
setTransportModalDayId(null)
|
||||
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.unknownError')) }
|
||||
}
|
||||
|
||||
@@ -1194,7 +1199,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
<TripFormModal isOpen={showTripForm} onClose={() => setShowTripForm(false)} onSave={async (data) => { await tripActions.updateTrip(tripId, data); toast.success(t('trip.toast.tripUpdated')) }} trip={trip} />
|
||||
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} />
|
||||
<ReservationModal isOpen={showReservationModal} onClose={() => { setShowReservationModal(false); setEditingReservation(null); setBookingForAssignmentId(null) }} onSave={handleSaveReservation} reservation={editingReservation} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={canUploadFiles ? (fd) => tripActions.addFile(tripId, fd) : undefined} onFileDelete={(id) => tripActions.deleteFile(tripId, id)} accommodations={tripAccommodations} defaultAssignmentId={bookingForAssignmentId} />
|
||||
{showTransportModal && <TransportModal isOpen={showTransportModal} onClose={() => { setShowTransportModal(false); setEditingTransport(null); setTransportModalDayId(null) }} onSave={handleSaveTransport} reservation={editingTransport} days={days} selectedDayId={transportModalDayId} />}
|
||||
{showTransportModal && <TransportModal isOpen={showTransportModal} onClose={() => { setShowTransportModal(false); setEditingTransport(null); setTransportModalDayId(null) }} onSave={handleSaveTransport} reservation={editingTransport} days={days} selectedDayId={transportModalDayId} files={files} onFileUpload={canUploadFiles ? (fd) => tripActions.addFile(tripId, fd) : undefined} onFileDelete={(id) => tripActions.deleteFile(tripId, id)} />}
|
||||
<ConfirmDialog
|
||||
isOpen={!!deletePlaceId}
|
||||
onClose={() => setDeletePlaceId(null)}
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Trip {
|
||||
export interface Day {
|
||||
id: number
|
||||
trip_id: number
|
||||
day_number?: number
|
||||
date: string
|
||||
title: string | null
|
||||
notes: string | null
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Day } from '../types'
|
||||
|
||||
export const getDayOrder = (day: Day, days: Day[]): number =>
|
||||
day.day_number ?? days.indexOf(day)
|
||||
|
||||
export const isDayInAccommodationRange = (
|
||||
day: Day,
|
||||
startDayId: number,
|
||||
endDayId: number,
|
||||
days: Day[],
|
||||
): boolean => {
|
||||
const startDay = days.find(d => d.id === startDayId)
|
||||
const endDay = days.find(d => d.id === endDayId)
|
||||
if (!startDay || !endDay) {
|
||||
// Endpoint days not in the loaded array (e.g. sparse test data or partial load).
|
||||
// Fall back to numeric ID range — acceptable since non-monotonic IDs only arise when
|
||||
// both endpoints are present in a fully-loaded trip's days list.
|
||||
return day.id >= Math.min(startDayId, endDayId) && day.id <= Math.max(startDayId, endDayId)
|
||||
}
|
||||
const lo = Math.min(getDayOrder(startDay, days), getDayOrder(endDay, days))
|
||||
const hi = Math.max(getDayOrder(startDay, days), getDayOrder(endDay, days))
|
||||
return getDayOrder(day, days) >= lo && getDayOrder(day, days) <= hi
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# 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
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](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).
|
||||
@@ -1,35 +0,0 @@
|
||||
// @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" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
-11956
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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 {}
|
||||
@@ -1,20 +0,0 @@
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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(),
|
||||
],
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
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 {}
|
||||
@@ -1,26 +0,0 @@
|
||||
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',
|
||||
}),
|
||||
);
|
||||
@@ -1,103 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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();
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user