diff --git a/tsconfig.json b/jsconfig.json similarity index 86% rename from tsconfig.json rename to jsconfig.json index 3833ef2..4344710 100644 --- a/tsconfig.json +++ b/jsconfig.json @@ -1,8 +1,8 @@ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "allowJs": false, - "checkJs": false, + "allowJs": true, + "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, diff --git a/package.json b/package.json index 8c99faf..60144fc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", "test": "vitest run", "test:e2e": "playwright test" }, diff --git a/src/hooks.server.ts b/src/hooks.server.js similarity index 56% rename from src/hooks.server.ts rename to src/hooks.server.js index a6be540..58e30c9 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.js @@ -1,7 +1,7 @@ -import type { Handle } from '@sveltejs/kit'; import { getSessionUser } from '$lib/server/auth/session'; -export const handle: Handle = async ({ event, resolve }) => { +/** @type {import('@sveltejs/kit').Handle} */ +export const handle = async ({ event, resolve }) => { event.locals.user = await getSessionUser(event); return resolve(event); }; diff --git a/src/lib/server/auth/admin-roles.ts b/src/lib/server/auth/admin-roles.js similarity index 64% rename from src/lib/server/auth/admin-roles.ts rename to src/lib/server/auth/admin-roles.js index c9463d3..1b0cbeb 100644 --- a/src/lib/server/auth/admin-roles.ts +++ b/src/lib/server/auth/admin-roles.js @@ -1,8 +1,9 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; import { env as privateEnv } from '$env/dynamic/private'; -import type { AdminScope } from '$lib/shared/types/domain'; import { requireVerifiedUser } from './session'; +/** @typedef {import('$lib/shared/types/domain').AdminScope} AdminScope */ + const defaultAdminEmails = ['minhtienit99@gmail.com', 'minhnguyetawf@gmail.com']; export function configuredAdminEmails() { @@ -10,18 +11,30 @@ export function configuredAdminEmails() { return fromEnv?.length ? fromEnv : defaultAdminEmails; } -export function scopesForEmail(email: string): AdminScope[] { +/** + * @param {string} email + * @returns {AdminScope[]} + */ +export function scopesForEmail(email) { if (!configuredAdminEmails().includes(email.toLowerCase())) return []; return ['global', 'vutrudodac', 'phienchotrenmay']; } -export function hasAdminScope(user: App.ArtemisUser | null, scope: AdminScope) { +/** + * @param {App.ArtemisUser | null} user + * @param {AdminScope} scope + */ +export function hasAdminScope(user, scope) { if (!user) return false; const scopes = scopesForEmail(user.email); return scopes.includes('global') || scopes.includes(scope); } -export async function requireAdmin(event: RequestEvent, scope: AdminScope) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {AdminScope} scope + */ +export async function requireAdmin(event, scope) { const user = await requireVerifiedUser(event); if (!hasAdminScope(user, scope)) { throw error(403, 'Bạn không có quyền admin cho khu vực này.'); diff --git a/src/lib/server/auth/session.ts b/src/lib/server/auth/session.js similarity index 74% rename from src/lib/server/auth/session.ts rename to src/lib/server/auth/session.js index ccf0448..43c228b 100644 --- a/src/lib/server/auth/session.ts +++ b/src/lib/server/auth/session.js @@ -1,14 +1,20 @@ -import { fail, redirect, type RequestEvent } from '@sveltejs/kit'; +import { fail, redirect } from '@sveltejs/kit'; import { env as privateEnv } from '$env/dynamic/private'; import { createSupabaseServerClient, hasSupabaseConfig } from '$lib/server/supabase/client'; const defaultDevEmail = 'minhtienit99@gmail.com'; -function domainFromEmail(email: string) { +/** @param {string} email */ +function domainFromEmail(email) { return email.split('@')[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, '') || 'starter'; } -function makeUser(email: string, id = `dev-${domainFromEmail(email)}`): App.ArtemisUser { +/** + * @param {string} email + * @param {string} [id] + * @returns {App.ArtemisUser} + */ +function makeUser(email, id = `dev-${domainFromEmail(email)}`) { const domain = domainFromEmail(email); return { id, @@ -20,12 +26,17 @@ function makeUser(email: string, id = `dev-${domainFromEmail(email)}`): App.Arte }; } -export function getFallbackDevUser(): App.ArtemisUser { +/** @returns {App.ArtemisUser} */ +export function getFallbackDevUser() { const configured = privateEnv.ARTEMIS_DEV_USER_EMAIL || privateEnv.ARTEMIS_ADMIN_EMAILS?.split(',')[0]; return makeUser((configured || defaultDevEmail).trim().toLowerCase()); } -export async function getSessionUser(event: RequestEvent): Promise { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @returns {Promise} + */ +export async function getSessionUser(event) { if (!hasSupabaseConfig()) return getFallbackDevUser(); const supabase = createSupabaseServerClient(event); @@ -58,14 +69,16 @@ export async function getSessionUser(event: RequestEvent): Promise token.length > 1 && !stopWords.has(token)); } -function overlapScore(left: string[], right: string[]) { +/** + * @param {string[]} left + * @param {string[]} right + */ +function overlapScore(left, right) { if (!left.length || !right.length) return 0; const rightSet = new Set(right); const shared = left.filter((token) => rightSet.has(token)); return shared.length / Math.max(left.length, right.length); } -function timeHintScore(left: string, right: string) { +/** + * @param {string} left + * @param {string} right + */ +function timeHintScore(left, right) { const leftTokens = tokenizeReport(left); const rightTokens = tokenizeReport(right); return overlapScore(leftTokens, rightTokens); } -export function scoreLostFoundMatch(lost: MatchableReport, found: MatchableReport): MatchScore { +/** + * @param {MatchableReport} lost + * @param {MatchableReport} found + * @returns {MatchScore} + */ +export function scoreLostFoundMatch(lost, found) { const lostDescription = tokenizeReport(lost.description); const foundDescription = tokenizeReport(found.description); const description = overlapScore(lostDescription, foundDescription); @@ -80,7 +100,8 @@ export function scoreLostFoundMatch(lost: MatchableReport, found: MatchableRepor const location = found.location ? overlapScore(tokenizeReport(lost.description), tokenizeReport(found.location)) : 0; const score = Math.round(Math.min(1, description * 0.72 + time * 0.18 + location * 0.1) * 100); - const level: MatchLevel = score >= 62 ? 'strong' : score >= 34 ? 'near' : 'none'; + /** @type {MatchLevel} */ + const level = score >= 62 ? 'strong' : score >= 34 ? 'near' : 'none'; const reasons = [ description > 0 ? 'mô tả có tín hiệu trùng' : '', time > 0 ? 'thời gian gần nhau' : '', @@ -90,7 +111,12 @@ export function scoreLostFoundMatch(lost: MatchableReport, found: MatchableRepor return { score, level, reasons }; } -export function buildMatchCandidate(lost: LostItem, found: FoundItem): MatchCandidate | null { +/** + * @param {LostItem} lost + * @param {FoundItem} found + * @returns {MatchCandidate | null} + */ +export function buildMatchCandidate(lost, found) { const result = scoreLostFoundMatch( { id: lost.id, @@ -118,8 +144,13 @@ export function buildMatchCandidate(lost: LostItem, found: FoundItem): MatchCand }; } -export function findCandidateMatches(lostItems: LostItem[], foundItems: FoundItem[]) { - const candidates: MatchCandidate[] = []; +/** + * @param {LostItem[]} lostItems + * @param {FoundItem[]} foundItems + */ +export function findCandidateMatches(lostItems, foundItems) { + /** @type {MatchCandidate[]} */ + const candidates = []; for (const lost of lostItems) { if (!['open', 'matched'].includes(lost.status)) continue; diff --git a/src/lib/server/domain/lost-found/matching.test.ts b/src/lib/server/domain/lost-found/matching.test.js similarity index 100% rename from src/lib/server/domain/lost-found/matching.test.ts rename to src/lib/server/domain/lost-found/matching.test.js diff --git a/src/lib/server/domain/marketplace/ranking.ts b/src/lib/server/domain/marketplace/ranking.js similarity index 70% rename from src/lib/server/domain/marketplace/ranking.ts rename to src/lib/server/domain/marketplace/ranking.js index 4342479..7e05588 100644 --- a/src/lib/server/domain/marketplace/ranking.ts +++ b/src/lib/server/domain/marketplace/ranking.js @@ -1,11 +1,16 @@ -import type { MarketplaceListing } from '$lib/shared/types/domain'; import { normalizeSearchText, tokenizeReport } from '$lib/server/domain/lost-found/matching'; -export interface RankedListing extends MarketplaceListing { - rankScore: number; -} +/** @typedef {import('$lib/shared/types/domain').MarketplaceListing} MarketplaceListing */ -export function marketplaceSearchScore(listing: MarketplaceListing, query: string) { +/** + * @typedef {MarketplaceListing & { rankScore: number }} RankedListing + */ + +/** + * @param {MarketplaceListing} listing + * @param {string} query + */ +export function marketplaceSearchScore(listing, query) { const normalizedQuery = normalizeSearchText(query); if (!normalizedQuery) return listing.careCount; @@ -18,7 +23,12 @@ export function marketplaceSearchScore(listing: MarketplaceListing, query: strin return exactNameBoost + tokenScore + Math.min(20, listing.careCount * 2); } -export function rankMarketplaceListings(listings: MarketplaceListing[], query = ''): RankedListing[] { +/** + * @param {MarketplaceListing[]} listings + * @param {string} [query] + * @returns {RankedListing[]} + */ +export function rankMarketplaceListings(listings, query = '') { return listings .filter((listing) => listing.status === 'approved') .map((listing) => ({ ...listing, rankScore: marketplaceSearchScore(listing, query) })) diff --git a/src/lib/server/domain/marketplace/ranking.test.ts b/src/lib/server/domain/marketplace/ranking.test.js similarity index 86% rename from src/lib/server/domain/marketplace/ranking.test.ts rename to src/lib/server/domain/marketplace/ranking.test.js index 7b27424..4d7bdca 100644 --- a/src/lib/server/domain/marketplace/ranking.test.ts +++ b/src/lib/server/domain/marketplace/ranking.test.js @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { rankMarketplaceListings } from './ranking'; -import type { MarketplaceListing } from '$lib/shared/types/domain'; + +/** @typedef {import('$lib/shared/types/domain').MarketplaceListing} MarketplaceListing */ const owner = { id: 'starter-1', @@ -10,7 +11,11 @@ const owner = { verifiedEmail: true }; -function listing(overrides: Partial): MarketplaceListing { +/** + * @param {Partial} overrides + * @returns {MarketplaceListing} + */ +function listing(overrides) { return { id: 'listing-1', owner, diff --git a/src/lib/server/persistence/memory-store.ts b/src/lib/server/persistence/memory-store.js similarity index 68% rename from src/lib/server/persistence/memory-store.ts rename to src/lib/server/persistence/memory-store.js index ab8c067..b7d8c55 100644 --- a/src/lib/server/persistence/memory-store.ts +++ b/src/lib/server/persistence/memory-store.js @@ -1,43 +1,45 @@ -import type { - AdminAction, - AdminScope, - FoundItem, - ImageMetadata, - ListingStatus, - LostItem, - MarketplaceListing, - MatchCandidate, - Notification, - NotificationType, - ProfileSummary, - ReportStatus -} from '$lib/shared/types/domain'; import { findCandidateMatches } from '$lib/server/domain/lost-found/matching'; +/** @typedef {import('$lib/shared/types/domain').AdminAction} AdminAction */ +/** @typedef {import('$lib/shared/types/domain').AdminScope} AdminScope */ +/** @typedef {import('$lib/shared/types/domain').FoundItem} FoundItem */ +/** @typedef {import('$lib/shared/types/domain').ImageMetadata} ImageMetadata */ +/** @typedef {import('$lib/shared/types/domain').ListingStatus} ListingStatus */ +/** @typedef {import('$lib/shared/types/domain').LostItem} LostItem */ +/** @typedef {import('$lib/shared/types/domain').MarketplaceListing} MarketplaceListing */ +/** @typedef {import('$lib/shared/types/domain').MatchCandidate} MatchCandidate */ +/** @typedef {import('$lib/shared/types/domain').Notification} Notification */ +/** @typedef {import('$lib/shared/types/domain').NotificationType} NotificationType */ +/** @typedef {import('$lib/shared/types/domain').ProfileSummary} ProfileSummary */ +/** @typedef {import('$lib/shared/types/domain').ReportStatus} ReportStatus */ + function randomId() { return globalThis.crypto.randomUUID(); } -interface MarketplaceInterest { - listingId: string; - profileId: string; - createdAt: string; -} +/** + * @typedef {object} MarketplaceInterest + * @property {string} listingId + * @property {string} profileId + * @property {string} createdAt + */ -export interface ArtemisMemoryState { - profiles: ProfileSummary[]; - lostItems: LostItem[]; - foundItems: FoundItem[]; - matchCandidates: MatchCandidate[]; - listings: MarketplaceListing[]; - interests: MarketplaceInterest[]; - notifications: Notification[]; - adminActions: AdminAction[]; -} +/** + * @typedef {object} ArtemisMemoryState + * @property {ProfileSummary[]} profiles + * @property {LostItem[]} lostItems + * @property {FoundItem[]} foundItems + * @property {MatchCandidate[]} matchCandidates + * @property {MarketplaceListing[]} listings + * @property {MarketplaceInterest[]} interests + * @property {Notification[]} notifications + * @property {AdminAction[]} adminActions + */ const now = () => new Date().toISOString(); -const starterProfile: ProfileSummary = { +/** @type {ProfileSummary} */ +const starterProfile = { id: 'seed-starter', email: 'starter@example.com', displayName: 'starter', @@ -45,7 +47,8 @@ const starterProfile: ProfileSummary = { verifiedEmail: true }; -function emptyState(): ArtemisMemoryState { +/** @returns {ArtemisMemoryState} */ +function emptyState() { return { profiles: [starterProfile], lostItems: [ @@ -100,7 +103,11 @@ function emptyState(): ArtemisMemoryState { const memoryState = emptyState(); memoryState.matchCandidates = findCandidateMatches(memoryState.lostItems, memoryState.foundItems); -function toProfile(user: App.ArtemisUser): ProfileSummary { +/** + * @param {App.ArtemisUser} user + * @returns {ProfileSummary} + */ +function toProfile(user) { return { id: user.id, email: user.email, @@ -110,7 +117,8 @@ function toProfile(user: App.ArtemisUser): ProfileSummary { }; } -function ensureProfile(user: App.ArtemisUser) { +/** @param {App.ArtemisUser} user */ +function ensureProfile(user) { const existing = memoryState.profiles.find((profile) => profile.id === user.id); const next = toProfile(user); @@ -123,7 +131,8 @@ function ensureProfile(user: App.ArtemisUser) { return next; } -function upsertMatchCandidates(candidates: MatchCandidate[]) { +/** @param {MatchCandidate[]} candidates */ +function upsertMatchCandidates(candidates) { for (const candidate of candidates) { const existing = memoryState.matchCandidates.find( (match) => match.lostItemId === candidate.lostItemId && match.foundItemId === candidate.foundItemId @@ -153,15 +162,15 @@ export function resetMemoryStateForTests() { memoryState.adminActions = next.adminActions; } -export function createMemoryLostItem(user: App.ArtemisUser, input: { - id?: string; - description: string; - lostAtText: string; - image?: ImageMetadata; -}) { +/** + * @param {App.ArtemisUser} user + * @param {{ id?: string, description: string, lostAtText: string, image?: ImageMetadata }} input + */ +export function createMemoryLostItem(user, input) { const profile = ensureProfile(user); const timestamp = now(); - const item: LostItem = { + /** @type {LostItem} */ + const item = { id: input.id ?? randomId(), owner: profile, description: input.description, @@ -187,16 +196,15 @@ export function createMemoryLostItem(user: App.ArtemisUser, input: { return { item, candidates }; } -export function createMemoryFoundItem(user: App.ArtemisUser, input: { - id?: string; - description: string; - foundAtText: string; - location: string; - image?: ImageMetadata; -}) { +/** + * @param {App.ArtemisUser} user + * @param {{ id?: string, description: string, foundAtText: string, location: string, image?: ImageMetadata }} input + */ +export function createMemoryFoundItem(user, input) { const profile = ensureProfile(user); const timestamp = now(); - const item: FoundItem = { + /** @type {FoundItem} */ + const item = { id: input.id ?? randomId(), finder: profile, description: input.description, @@ -225,7 +233,8 @@ export function createMemoryFoundItem(user: App.ArtemisUser, input: { return { item, candidates }; } -export function listMemoryLostFound(user?: App.ArtemisUser | null) { +/** @param {App.ArtemisUser | null} [user] */ +export function listMemoryLostFound(user) { const profileId = user?.id; return { lostItems: memoryState.lostItems.filter((item) => item.status !== 'hidden').slice(0, 20), @@ -237,12 +246,13 @@ export function listMemoryLostFound(user?: App.ArtemisUser | null) { }; } -export function updateMemoryReportStatus( - kind: 'lost' | 'found', - id: string, - status: ReportStatus, - actor: App.ArtemisUser -) { +/** + * @param {'lost' | 'found'} kind + * @param {string} id + * @param {ReportStatus} status + * @param {App.ArtemisUser} actor + */ +export function updateMemoryReportStatus(kind, id, status, actor) { const collection = kind === 'lost' ? memoryState.lostItems : memoryState.foundItems; const item = collection.find((entry) => entry.id === id); if (!item) return null; @@ -252,18 +262,15 @@ export function updateMemoryReportStatus( return item; } -export function createMemoryListing(user: App.ArtemisUser, input: { - id?: string; - name: string; - quantity: number; - description: string; - priceText: string; - contact: string; - image?: ImageMetadata; -}) { +/** + * @param {App.ArtemisUser} user + * @param {{ id?: string, name: string, quantity: number, description: string, priceText: string, contact: string, image?: ImageMetadata }} input + */ +export function createMemoryListing(user, input) { const profile = ensureProfile(user); const timestamp = now(); - const listing: MarketplaceListing = { + /** @type {MarketplaceListing} */ + const listing = { id: input.id ?? randomId(), owner: profile, name: input.name, @@ -286,7 +293,8 @@ export function createMemoryListing(user: App.ArtemisUser, input: { return listing; } -export function listMemoryMarketplace(user?: App.ArtemisUser | null) { +/** @param {App.ArtemisUser | null} [user] */ +export function listMemoryMarketplace(user) { const profileId = user?.id; return memoryState.listings.map((listing) => ({ ...listing, @@ -300,7 +308,11 @@ export function listMemoryMarketplace(user?: App.ArtemisUser | null) { })); } -export function toggleMemoryMarketplaceInterest(user: App.ArtemisUser, listingId: string) { +/** + * @param {App.ArtemisUser} user + * @param {string} listingId + */ +export function toggleMemoryMarketplaceInterest(user, listingId) { ensureProfile(user); const listing = memoryState.listings.find((entry) => entry.id === listingId); if (!listing) return null; @@ -326,7 +338,12 @@ export function toggleMemoryMarketplaceInterest(user: App.ArtemisUser, listingId }; } -export function updateMemoryListingStatus(id: string, status: ListingStatus, actor: App.ArtemisUser) { +/** + * @param {string} id + * @param {ListingStatus} status + * @param {App.ArtemisUser} actor + */ +export function updateMemoryListingStatus(id, status, actor) { const listing = memoryState.listings.find((entry) => entry.id === id); if (!listing) return null; listing.status = status; @@ -339,17 +356,19 @@ export function updateMemoryListingStatus(id: string, status: ListingStatus, act return listing; } -export function createMemoryNotification( - recipientId: string, - type: NotificationType, - message: string, - payload: Record -) { +/** + * @param {string} recipientId + * @param {NotificationType} type + * @param {string} message + * @param {Record} payload + */ +export function createMemoryNotification(recipientId, type, message, payload) { const deliveryKey = `${type}:${recipientId}:${JSON.stringify(payload)}`; const existing = memoryState.notifications.find((notification) => notification.deliveryKey === deliveryKey); if (existing) return existing; - const notification: Notification = { + /** @type {Notification} */ + const notification = { id: randomId(), recipientId, type, @@ -363,7 +382,11 @@ export function createMemoryNotification( return notification; } -export function markMemoryNotificationRead(recipientId: string, notificationId: string) { +/** + * @param {string} recipientId + * @param {string} notificationId + */ +export function markMemoryNotificationRead(recipientId, notificationId) { const notification = memoryState.notifications.find( (entry) => entry.id === notificationId && entry.recipientId === recipientId ); @@ -372,15 +395,17 @@ export function markMemoryNotificationRead(recipientId: string, notificationId: return notification; } -export function createMemoryAdminAction( - actor: App.ArtemisUser, - scope: AdminScope, - action: string, - targetType: string, - targetId: string, - payload: Record -) { - const adminAction: AdminAction = { +/** + * @param {App.ArtemisUser} actor + * @param {AdminScope} scope + * @param {string} action + * @param {string} targetType + * @param {string} targetId + * @param {Record} payload + */ +export function createMemoryAdminAction(actor, scope, action, targetType, targetId, payload) { + /** @type {AdminAction} */ + const adminAction = { id: randomId(), actor: ensureProfile(actor), scope, diff --git a/src/lib/server/persistence/memory-store.test.ts b/src/lib/server/persistence/memory-store.test.js similarity index 95% rename from src/lib/server/persistence/memory-store.test.ts rename to src/lib/server/persistence/memory-store.test.js index 20e2cd0..6e4ccbb 100644 --- a/src/lib/server/persistence/memory-store.test.ts +++ b/src/lib/server/persistence/memory-store.test.js @@ -6,13 +6,14 @@ import { updateMemoryListingStatus } from './memory-store'; +/** @type {App.ArtemisUser} */ const user = { id: 'dev-user', email: 'dev@example.com', displayName: 'dev', domain: 'dev', verifiedEmail: true, - authProvider: 'google' as const + authProvider: 'google' }; describe('memory store idempotency helpers', () => { diff --git a/src/lib/server/repositories/lost-found.ts b/src/lib/server/repositories/lost-found.js similarity index 65% rename from src/lib/server/repositories/lost-found.ts rename to src/lib/server/repositories/lost-found.js index 867c194..0c4c88a 100644 --- a/src/lib/server/repositories/lost-found.ts +++ b/src/lib/server/repositories/lost-found.js @@ -1,4 +1,4 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; import { createSupabaseServerClient, createSupabaseServiceClient, @@ -12,12 +12,21 @@ import { updateMemoryReportStatus } from '$lib/server/persistence/memory-store'; import { findCandidateMatches } from '$lib/server/domain/lost-found/matching'; -import type { FoundItem, ImageMetadata, LostItem, ProfileSummary, ReportStatus } from '$lib/shared/types/domain'; -type ReportKind = 'lost' | 'found'; -type Row = Record; +/** @typedef {import('$lib/shared/types/domain').FoundItem} FoundItem */ +/** @typedef {import('$lib/shared/types/domain').ImageMetadata} ImageMetadata */ +/** @typedef {import('$lib/shared/types/domain').LostItem} LostItem */ +/** @typedef {import('$lib/shared/types/domain').ProfileSummary} ProfileSummary */ +/** @typedef {import('$lib/shared/types/domain').ReportStatus} ReportStatus */ -function profileFromUser(user: App.ArtemisUser): ProfileSummary { +/** @typedef {'lost' | 'found'} ReportKind */ +/** @typedef {Record} Row */ + +/** + * @param {App.ArtemisUser} user + * @returns {ProfileSummary} + */ +function profileFromUser(user) { return { id: user.id, email: user.email, @@ -27,8 +36,13 @@ function profileFromUser(user: App.ArtemisUser): ProfileSummary { }; } -function profileFromRow(row: Row, fallback?: ProfileSummary): ProfileSummary { - const profile = (row.profiles ?? row.profile) as Row | undefined; +/** + * @param {Row} row + * @param {ProfileSummary} [fallback] + * @returns {ProfileSummary} + */ +function profileFromRow(row, fallback) { + const profile = /** @type {Row | undefined} */ (row.profiles ?? row.profile); return { id: String(row.owner_profile_id ?? row.finder_profile_id ?? profile?.id ?? fallback?.id ?? 'unknown'), email: String(profile?.email ?? fallback?.email ?? 'unknown@example.com'), @@ -38,42 +52,60 @@ function profileFromRow(row: Row, fallback?: ProfileSummary): ProfileSummary { }; } -function imageFromRow(row: Row): ImageMetadata | undefined { +/** + * @param {Row} row + * @returns {ImageMetadata | undefined} + */ +function imageFromRow(row) { const metadata = row.image_metadata; if (!metadata || typeof metadata !== 'object') return undefined; - return metadata as ImageMetadata; + return /** @type {ImageMetadata} */ (metadata); } -function lostFromRow(row: Row, fallback?: ProfileSummary): LostItem { +/** + * @param {Row} row + * @param {ProfileSummary} [fallback] + * @returns {LostItem} + */ +function lostFromRow(row, fallback) { return { id: String(row.id), owner: profileFromRow(row, fallback), description: String(row.description ?? ''), lostAtText: String(row.occurred_at_text ?? ''), - status: String(row.status ?? 'open') as ReportStatus, + status: /** @type {ReportStatus} */ (String(row.status ?? 'open')), image: imageFromRow(row), - payload: (row.payload as Record) ?? {}, + payload: /** @type {Record} */ (row.payload) ?? {}, createdAt: String(row.created_at ?? new Date().toISOString()), updatedAt: String(row.updated_at ?? row.created_at ?? new Date().toISOString()) }; } -function foundFromRow(row: Row, fallback?: ProfileSummary): FoundItem { +/** + * @param {Row} row + * @param {ProfileSummary} [fallback] + * @returns {FoundItem} + */ +function foundFromRow(row, fallback) { return { id: String(row.id), finder: profileFromRow(row, fallback), description: String(row.description ?? ''), foundAtText: String(row.occurred_at_text ?? ''), location: String(row.location ?? ''), - status: String(row.status ?? 'open') as ReportStatus, + status: /** @type {ReportStatus} */ (String(row.status ?? 'open')), image: imageFromRow(row), - payload: (row.payload as Record) ?? {}, + payload: /** @type {Record} */ (row.payload) ?? {}, createdAt: String(row.created_at ?? new Date().toISOString()), updatedAt: String(row.updated_at ?? row.created_at ?? new Date().toISOString()) }; } -async function ensureSupabaseProfile(event: RequestEvent, user: App.ArtemisUser) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + */ +async function ensureSupabaseProfile(event, user) { const supabase = createSupabaseServerClient(event); const { error: profileError } = await supabase.from('profiles').upsert( { @@ -90,7 +122,11 @@ async function ensureSupabaseProfile(event: RequestEvent, user: App.ArtemisUser) if (profileError) throw error(500, `Không thể đồng bộ profile Artemis: ${profileError.message}`); } -async function listSupabaseReports(event: RequestEvent, user?: App.ArtemisUser | null) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser | null} [user] + */ +async function listSupabaseReports(event, user) { const supabase = createSupabaseServerClient(event); const [lostResponse, foundResponse, notificationResponse] = await Promise.all([ supabase @@ -119,9 +155,9 @@ async function listSupabaseReports(event: RequestEvent, user?: App.ArtemisUser | return { ...listMemoryLostFound(user), warning: 'Supabase chưa sẵn sàng, Artemis đang dùng dữ liệu local.' }; } - const lostItems = (lostResponse.data ?? []).map((row) => lostFromRow(row as Row, user ? profileFromUser(user) : undefined)); + const lostItems = (lostResponse.data ?? []).map((row) => lostFromRow(/** @type {Row} */ (row), user ? profileFromUser(user) : undefined)); const foundItems = (foundResponse.data ?? []).map((row) => - foundFromRow(row as Row, user ? profileFromUser(user) : undefined) + foundFromRow(/** @type {Row} */ (row), user ? profileFromUser(user) : undefined) ); return { @@ -135,23 +171,27 @@ async function listSupabaseReports(event: RequestEvent, user?: App.ArtemisUser | message: String(row.message), readAt: row.read_at ? String(row.read_at) : null, deliveryKey: String(row.delivery_key), - payload: (row.payload as Record) ?? {}, + payload: /** @type {Record} */ (row.payload) ?? {}, createdAt: String(row.created_at) })) }; } -export async function listLostFoundDashboard(event: RequestEvent, user?: App.ArtemisUser | null) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser | null} [user] + */ +export async function listLostFoundDashboard(event, user) { if (!hasSupabaseConfig()) return listMemoryLostFound(user); return listSupabaseReports(event, user); } -export async function createLostReport(event: RequestEvent, user: App.ArtemisUser, input: { - id: string; - description: string; - lostAtText: string; - image?: ImageMetadata; -}) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {{ id: string, description: string, lostAtText: string, image?: ImageMetadata }} input + */ +export async function createLostReport(event, user, input) { if (!hasSupabaseConfig()) return createMemoryLostItem(user, input); await ensureSupabaseProfile(event, user); @@ -171,16 +211,15 @@ export async function createLostReport(event: RequestEvent, user: App.ArtemisUse .single(); if (insertError) throw error(500, `Không thể gửi tín hiệu tìm đồ: ${insertError.message}`); - return { item: lostFromRow(data as Row, profileFromUser(user)), candidates: [] }; + return { item: lostFromRow(/** @type {Row} */ (data), profileFromUser(user)), candidates: [] }; } -export async function createFoundReport(event: RequestEvent, user: App.ArtemisUser, input: { - id: string; - description: string; - foundAtText: string; - location: string; - image?: ImageMetadata; -}) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {{ id: string, description: string, foundAtText: string, location: string, image?: ImageMetadata }} input + */ +export async function createFoundReport(event, user, input) { if (!hasSupabaseConfig()) return createMemoryFoundItem(user, input); await ensureSupabaseProfile(event, user); @@ -201,10 +240,11 @@ export async function createFoundReport(event: RequestEvent, user: App.ArtemisUs .single(); if (insertError) throw error(500, `Không thể gửi tín hiệu trả đồ: ${insertError.message}`); - return { item: foundFromRow(data as Row, profileFromUser(user)), candidates: [] }; + return { item: foundFromRow(/** @type {Row} */ (data), profileFromUser(user)), candidates: [] }; } -export async function listLostFoundAdmin(event: RequestEvent) { +/** @param {import('@sveltejs/kit').RequestEvent} event */ +export async function listLostFoundAdmin(event) { const dashboard = await listLostFoundDashboard(event, event.locals.user); return { ...dashboard, @@ -214,13 +254,14 @@ export async function listLostFoundAdmin(event: RequestEvent) { }; } -export async function updateReportStatus( - event: RequestEvent, - actor: App.ArtemisUser, - kind: ReportKind, - id: string, - status: ReportStatus -) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} actor + * @param {ReportKind} kind + * @param {string} id + * @param {ReportStatus} status + */ +export async function updateReportStatus(event, actor, kind, id, status) { if (!hasSupabaseConfig()) return updateMemoryReportStatus(kind, id, status, actor); const supabase = hasSupabaseServiceConfig() ? createSupabaseServiceClient() : createSupabaseServerClient(event); @@ -233,5 +274,5 @@ export async function updateReportStatus( .single(); if (updateError) throw error(500, `Không thể cập nhật trạng thái tín hiệu: ${updateError.message}`); - return kind === 'lost' ? lostFromRow(data as Row) : foundFromRow(data as Row); + return kind === 'lost' ? lostFromRow(/** @type {Row} */ (data)) : foundFromRow(/** @type {Row} */ (data)); } diff --git a/src/lib/server/repositories/marketplace.ts b/src/lib/server/repositories/marketplace.js similarity index 65% rename from src/lib/server/repositories/marketplace.ts rename to src/lib/server/repositories/marketplace.js index 956b1ed..bde6136 100644 --- a/src/lib/server/repositories/marketplace.ts +++ b/src/lib/server/repositories/marketplace.js @@ -1,4 +1,4 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; import { createSupabaseServerClient, createSupabaseServiceClient, @@ -12,16 +12,19 @@ import { updateMemoryListingStatus } from '$lib/server/persistence/memory-store'; import { rankMarketplaceListings } from '$lib/server/domain/marketplace/ranking'; -import type { - ImageMetadata, - ListingStatus, - MarketplaceListing, - ProfileSummary -} from '$lib/shared/types/domain'; -type Row = Record; +/** @typedef {import('$lib/shared/types/domain').ImageMetadata} ImageMetadata */ +/** @typedef {import('$lib/shared/types/domain').ListingStatus} ListingStatus */ +/** @typedef {import('$lib/shared/types/domain').MarketplaceListing} MarketplaceListing */ +/** @typedef {import('$lib/shared/types/domain').ProfileSummary} ProfileSummary */ -function profileFromUser(user: App.ArtemisUser): ProfileSummary { +/** @typedef {Record} Row */ + +/** + * @param {App.ArtemisUser} user + * @returns {ProfileSummary} + */ +function profileFromUser(user) { return { id: user.id, email: user.email, @@ -31,8 +34,13 @@ function profileFromUser(user: App.ArtemisUser): ProfileSummary { }; } -function profileFromRow(row: Row, fallback?: ProfileSummary): ProfileSummary { - const profile = (row.profiles ?? row.profile) as Row | undefined; +/** + * @param {Row} row + * @param {ProfileSummary} [fallback] + * @returns {ProfileSummary} + */ +function profileFromRow(row, fallback) { + const profile = /** @type {Row | undefined} */ (row.profiles ?? row.profile); return { id: String(row.owner_profile_id ?? profile?.id ?? fallback?.id ?? 'unknown'), email: String(profile?.email ?? fallback?.email ?? 'unknown@example.com'), @@ -42,13 +50,22 @@ function profileFromRow(row: Row, fallback?: ProfileSummary): ProfileSummary { }; } -function imageFromRow(row: Row): ImageMetadata | undefined { +/** + * @param {Row} row + * @returns {ImageMetadata | undefined} + */ +function imageFromRow(row) { const metadata = row.image_metadata; if (!metadata || typeof metadata !== 'object') return undefined; - return metadata as ImageMetadata; + return /** @type {ImageMetadata} */ (metadata); } -function listingFromRow(row: Row, currentUserId?: string): MarketplaceListing { +/** + * @param {Row} row + * @param {string} [currentUserId] + * @returns {MarketplaceListing} + */ +function listingFromRow(row, currentUserId) { const interests = Array.isArray(row.marketplace_interests) ? row.marketplace_interests : []; return { id: String(row.id), @@ -58,19 +75,23 @@ function listingFromRow(row: Row, currentUserId?: string): MarketplaceListing { description: String(row.description ?? ''), priceText: String(row.price_text ?? ''), contact: String(row.contact ?? ''), - status: String(row.status ?? 'pending') as ListingStatus, + status: /** @type {ListingStatus} */ (String(row.status ?? 'pending')), image: imageFromRow(row), careCount: Number(row.care_count ?? interests.length ?? 0), caredByCurrentUser: currentUserId - ? interests.some((interest) => String((interest as Row).profile_id) === currentUserId) + ? interests.some((interest) => String(/** @type {Row} */ (interest).profile_id) === currentUserId) : false, - payload: (row.payload as Record) ?? {}, + payload: /** @type {Record} */ (row.payload) ?? {}, createdAt: String(row.created_at ?? new Date().toISOString()), updatedAt: String(row.updated_at ?? row.created_at ?? new Date().toISOString()) }; } -async function ensureSupabaseProfile(event: RequestEvent, user: App.ArtemisUser) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + */ +async function ensureSupabaseProfile(event, user) { const supabase = createSupabaseServerClient(event); const { error: profileError } = await supabase.from('profiles').upsert( { @@ -87,7 +108,11 @@ async function ensureSupabaseProfile(event: RequestEvent, user: App.ArtemisUser) if (profileError) throw error(500, `Không thể đồng bộ profile Artemis: ${profileError.message}`); } -async function listSupabaseListings(event: RequestEvent, user?: App.ArtemisUser | null) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser | null} [user] + */ +async function listSupabaseListings(event, user) { const supabase = createSupabaseServerClient(event); const { data, error: listError } = await supabase .from('marketplace_listings') @@ -96,10 +121,15 @@ async function listSupabaseListings(event: RequestEvent, user?: App.ArtemisUser .limit(60); if (listError) return listMemoryMarketplace(user); - return (data ?? []).map((row) => listingFromRow(row as Row, user?.id)); + return (data ?? []).map((row) => listingFromRow(/** @type {Row} */ (row), user?.id)); } -export async function listMarketplace(event: RequestEvent, user?: App.ArtemisUser | null, query = '') { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser | null} [user] + * @param {string} [query] + */ +export async function listMarketplace(event, user, query = '') { const listings = hasSupabaseConfig() ? await listSupabaseListings(event, user) : listMemoryMarketplace(user); return { listings: rankMarketplaceListings(listings, query), @@ -107,15 +137,12 @@ export async function listMarketplace(event: RequestEvent, user?: App.ArtemisUse }; } -export async function createMarketplaceListing(event: RequestEvent, user: App.ArtemisUser, input: { - id: string; - name: string; - quantity: number; - description: string; - priceText: string; - contact: string; - image?: ImageMetadata; -}) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {{ id: string, name: string, quantity: number, description: string, priceText: string, contact: string, image?: ImageMetadata }} input + */ +export async function createMarketplaceListing(event, user, input) { if (!hasSupabaseConfig()) return createMemoryListing(user, input); await ensureSupabaseProfile(event, user); @@ -138,10 +165,15 @@ export async function createMarketplaceListing(event: RequestEvent, user: App.Ar .single(); if (insertError) throw error(500, `Không thể phóng vật phẩm lên chợ: ${insertError.message}`); - return listingFromRow(data as Row, user.id); + return listingFromRow(/** @type {Row} */ (data), user.id); } -export async function toggleMarketplaceCare(event: RequestEvent, user: App.ArtemisUser, listingId: string) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {string} listingId + */ +export async function toggleMarketplaceCare(event, user, listingId) { if (!hasSupabaseConfig()) return toggleMemoryMarketplaceInterest(user, listingId); await ensureSupabaseProfile(event, user); @@ -172,7 +204,8 @@ export async function toggleMarketplaceCare(event: RequestEvent, user: App.Artem return null; } -export async function listMarketplaceAdmin(event: RequestEvent) { +/** @param {import('@sveltejs/kit').RequestEvent} event */ +export async function listMarketplaceAdmin(event) { const listings = hasSupabaseConfig() ? await listSupabaseListings(event, event.locals.user) : listMemoryMarketplace(event.locals.user); @@ -184,12 +217,13 @@ export async function listMarketplaceAdmin(event: RequestEvent) { }; } -export async function moderateMarketplaceListing( - event: RequestEvent, - actor: App.ArtemisUser, - listingId: string, - status: ListingStatus -) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} actor + * @param {string} listingId + * @param {ListingStatus} status + */ +export async function moderateMarketplaceListing(event, actor, listingId, status) { if (!hasSupabaseConfig()) return updateMemoryListingStatus(listingId, status, actor); const supabase = hasSupabaseServiceConfig() ? createSupabaseServiceClient() : createSupabaseServerClient(event); @@ -201,5 +235,5 @@ export async function moderateMarketplaceListing( .single(); if (updateError) throw error(500, `Không thể cập nhật vật phẩm: ${updateError.message}`); - return listingFromRow(data as Row, actor.id); + return listingFromRow(/** @type {Row} */ (data), actor.id); } diff --git a/src/lib/server/repositories/notifications.ts b/src/lib/server/repositories/notifications.js similarity index 73% rename from src/lib/server/repositories/notifications.ts rename to src/lib/server/repositories/notifications.js index 3457a74..93472fc 100644 --- a/src/lib/server/repositories/notifications.ts +++ b/src/lib/server/repositories/notifications.js @@ -1,8 +1,13 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; import { createSupabaseServerClient, hasSupabaseConfig } from '$lib/server/supabase/client'; import { markMemoryNotificationRead } from '$lib/server/persistence/memory-store'; -export async function markNotificationRead(event: RequestEvent, user: App.ArtemisUser, notificationId: string) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {string} notificationId + */ +export async function markNotificationRead(event, user, notificationId) { if (!hasSupabaseConfig()) return markMemoryNotificationRead(user.id, notificationId); const supabase = createSupabaseServerClient(event); diff --git a/src/lib/server/security/origin-check.ts b/src/lib/server/security/origin-check.js similarity index 56% rename from src/lib/server/security/origin-check.ts rename to src/lib/server/security/origin-check.js index 059a65d..129d225 100644 --- a/src/lib/server/security/origin-check.ts +++ b/src/lib/server/security/origin-check.js @@ -1,6 +1,7 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; -export function assertSameOrigin(event: RequestEvent) { +/** @param {import('@sveltejs/kit').RequestEvent} event */ +export function assertSameOrigin(event) { const origin = event.request.headers.get('origin'); if (!origin) return; if (origin !== event.url.origin) { diff --git a/src/lib/server/storage/image-storage.ts b/src/lib/server/storage/image-storage.js similarity index 64% rename from src/lib/server/storage/image-storage.ts rename to src/lib/server/storage/image-storage.js index 1f43d95..8b6e537 100644 --- a/src/lib/server/storage/image-storage.ts +++ b/src/lib/server/storage/image-storage.js @@ -1,13 +1,16 @@ -import { error, type RequestEvent } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; import { artemisStorageBucketPrefix, createSupabaseServerClient, hasSupabaseConfig } from '$lib/server/supabase/client'; import { imageUploadLimits } from '$lib/shared/constants/limits'; -import type { ImageMetadata, ProductScope } from '$lib/shared/types/domain'; -function sanitizeFileName(fileName: string) { +/** @typedef {import('$lib/shared/types/domain').ImageMetadata} ImageMetadata */ +/** @typedef {import('$lib/shared/types/domain').ProductScope} ProductScope */ + +/** @param {string} fileName */ +function sanitizeFileName(fileName) { const fallback = 'artemis-upload'; const safe = fileName .toLowerCase() @@ -17,14 +20,16 @@ function sanitizeFileName(fileName: string) { return safe || fallback; } -export function bucketForProduct(scope: ProductScope) { +/** @param {ProductScope} scope */ +export function bucketForProduct(scope) { return scope === 'phienchotrenmay' ? `${artemisStorageBucketPrefix}-marketplace-images` : `${artemisStorageBucketPrefix}-report-images`; } -export function assertValidImageFile(file: File) { - if (!imageUploadLimits.allowedMimeTypes.includes(file.type as (typeof imageUploadLimits.allowedMimeTypes)[number])) { +/** @param {File} file */ +export function assertValidImageFile(file) { + if (!imageUploadLimits.allowedMimeTypes.includes(/** @type {(typeof imageUploadLimits.allowedMimeTypes)[number]} */ (file.type))) { throw error(400, 'Ảnh cần là JPEG, PNG, WebP hoặc GIF.'); } @@ -33,19 +38,22 @@ export function assertValidImageFile(file: File) { } } -export async function uploadImageFromForm( - event: RequestEvent, - user: App.ArtemisUser, - scope: ProductScope, - recordId: string, - file: FormDataEntryValue | null -): Promise { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {App.ArtemisUser} user + * @param {ProductScope} scope + * @param {string} recordId + * @param {FormDataEntryValue | null} file + * @returns {Promise} + */ +export async function uploadImageFromForm(event, user, scope, recordId, file) { if (!(file instanceof File) || file.size === 0) return undefined; assertValidImageFile(file); const bucket = bucketForProduct(scope); const path = `${user.id}/${recordId}/${Date.now()}-${sanitizeFileName(file.name)}`; - const metadata: ImageMetadata = { + /** @type {ImageMetadata} */ + const metadata = { path, bucket, mimeType: file.type, @@ -69,7 +77,11 @@ export async function uploadImageFromForm( return metadata; } -export async function resolveImageUrl(event: RequestEvent, image?: ImageMetadata) { +/** + * @param {import('@sveltejs/kit').RequestEvent} event + * @param {ImageMetadata} [image] + */ +export async function resolveImageUrl(event, image) { if (!image) return undefined; if (image.url) return image.url; if (!hasSupabaseConfig()) return undefined; diff --git a/src/lib/server/supabase/client.ts b/src/lib/server/supabase/client.js similarity index 85% rename from src/lib/server/supabase/client.ts rename to src/lib/server/supabase/client.js index a7aa525..0f765bb 100644 --- a/src/lib/server/supabase/client.ts +++ b/src/lib/server/supabase/client.js @@ -1,6 +1,5 @@ -import { createServerClient, type CookieOptions } from '@supabase/ssr'; +import { createServerClient } from '@supabase/ssr'; import { createClient } from '@supabase/supabase-js'; -import type { RequestEvent } from '@sveltejs/kit'; import { env as privateEnv } from '$env/dynamic/private'; import { env as publicEnv } from '$env/dynamic/public'; @@ -16,7 +15,8 @@ export function hasSupabaseServiceConfig() { return Boolean(publicEnv.PUBLIC_SUPABASE_URL && privateEnv.SUPABASE_SERVICE_ROLE_KEY); } -export function createSupabaseServerClient(event: RequestEvent) { +/** @param {import('@sveltejs/kit').RequestEvent} event */ +export function createSupabaseServerClient(event) { const supabaseUrl = publicEnv.PUBLIC_SUPABASE_URL; const supabaseAnonKey = publicEnv.PUBLIC_SUPABASE_ANON_KEY; @@ -27,7 +27,8 @@ export function createSupabaseServerClient(event: RequestEvent) { return createServerClient(supabaseUrl, supabaseAnonKey, { cookies: { getAll: () => event.cookies.getAll(), - setAll: (cookiesToSet: { name: string; value: string; options: CookieOptions }[]) => { + /** @param {{ name: string, value: string, options: import('@supabase/ssr').CookieOptions }[]} cookiesToSet */ + setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: options.path ?? '/' }); }); diff --git a/src/lib/shared/constants/app.ts b/src/lib/shared/constants/app.js similarity index 86% rename from src/lib/shared/constants/app.ts rename to src/lib/shared/constants/app.js index 037e11e..f6ce88d 100644 --- a/src/lib/shared/constants/app.ts +++ b/src/lib/shared/constants/app.js @@ -1,10 +1,10 @@ export const appName = 'Artemis'; -export const publicRoutes = [ +export const publicRoutes = /** @type {const} */ ([ { href: '/', label: 'Artemis' }, { href: '/vutrudodac', label: 'Vũ trụ đồ đạc' }, { href: '/phienchotrenmay', label: 'Phiên chợ trên mây' }, { href: '/account', label: 'Account' } -] as const; +]); export const footerAttribution = 'Made by miti99 (miti99.com) from artemis (iamminhnguyet.com) idea, with <3'; diff --git a/src/lib/shared/constants/app.test.ts b/src/lib/shared/constants/app.test.js similarity index 100% rename from src/lib/shared/constants/app.test.ts rename to src/lib/shared/constants/app.test.js diff --git a/src/lib/shared/constants/limits.ts b/src/lib/shared/constants/limits.js similarity index 65% rename from src/lib/shared/constants/limits.ts rename to src/lib/shared/constants/limits.js index 3ddd1d1..c52bb00 100644 --- a/src/lib/shared/constants/limits.ts +++ b/src/lib/shared/constants/limits.js @@ -1,18 +1,18 @@ -export const imageUploadLimits = { +export const imageUploadLimits = /** @type {const} */ ({ maxBytes: 2 * 1024 * 1024, maxImagesPerRecord: 1, signedUrlTtlSeconds: 60 * 10, allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] -} as const; +}); -export const freshnessTargets = { +export const freshnessTargets = /** @type {const} */ ({ notificationPollMs: 15000, adminQueuePollMs: 15000, maxAcceptedFreshnessMs: 30000 -} as const; +}); -export const abuseLimits = { +export const abuseLimits = /** @type {const} */ ({ reportsPerUserPerDay: 20, listingsPerUserPerDay: 10, interestsPerUserPerDay: 60 -} as const; +}); diff --git a/src/lib/shared/types/database.ts b/src/lib/shared/types/database.d.ts similarity index 100% rename from src/lib/shared/types/database.ts rename to src/lib/shared/types/database.d.ts diff --git a/src/lib/shared/types/domain.ts b/src/lib/shared/types/domain.d.ts similarity index 100% rename from src/lib/shared/types/domain.ts rename to src/lib/shared/types/domain.d.ts diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.js similarity index 79% rename from src/routes/+layout.server.ts rename to src/routes/+layout.server.js index c42064c..9cba3ab 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.js @@ -1,8 +1,8 @@ -import type { LayoutServerLoad } from './$types'; import { hasAdminScope } from '$lib/server/auth/admin-roles'; import { hasSupabaseConfig } from '$lib/server/supabase/client'; -export const load: LayoutServerLoad = ({ locals, url }) => { +/** @type {import('./$types').LayoutServerLoad} */ +export const load = ({ locals, url }) => { return { currentPath: url.pathname, user: locals.user, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 48acc20..a852343 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,9 @@ -
diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.js similarity index 86% rename from src/routes/account/+page.server.ts rename to src/routes/account/+page.server.js index 280a0ec..a7d3e08 100644 --- a/src/routes/account/+page.server.ts +++ b/src/routes/account/+page.server.js @@ -1,8 +1,8 @@ import { redirect } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; import { createSupabaseServerClient, hasSupabaseConfig } from '$lib/server/supabase/client'; -export const load: PageServerLoad = ({ locals, url }) => { +/** @type {import('./$types').PageServerLoad} */ +export const load = ({ locals, url }) => { return { user: locals.user, reason: url.searchParams.get('reason'), @@ -10,7 +10,8 @@ export const load: PageServerLoad = ({ locals, url }) => { }; }; -export const actions: Actions = { +/** @type {import('./$types').Actions} */ +export const actions = { signIn: async (event) => { if (!hasSupabaseConfig()) { throw redirect(303, '/account'); diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 81683ed..07ff757 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -1,8 +1,8 @@ - diff --git a/src/routes/api/[...path]/+server.js b/src/routes/api/[...path]/+server.js new file mode 100644 index 0000000..7c1b5de --- /dev/null +++ b/src/routes/api/[...path]/+server.js @@ -0,0 +1,21 @@ +import { json } from '@sveltejs/kit'; + +const retired = () => + json( + { + status: 'retired', + message: 'Legacy /api/* routes were retired in the SvelteKit/Supabase cutover. Use page actions instead.' + }, + { status: 410 } + ); + +/** @type {import('./$types').RequestHandler} */ +export const GET = retired; +/** @type {import('./$types').RequestHandler} */ +export const POST = retired; +/** @type {import('./$types').RequestHandler} */ +export const PATCH = retired; +/** @type {import('./$types').RequestHandler} */ +export const PUT = retired; +/** @type {import('./$types').RequestHandler} */ +export const DELETE = retired; diff --git a/src/routes/api/[...path]/+server.ts b/src/routes/api/[...path]/+server.ts deleted file mode 100644 index 3de2b7e..0000000 --- a/src/routes/api/[...path]/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { json, type RequestHandler } from '@sveltejs/kit'; - -const retired = () => - json( - { - status: 'retired', - message: 'Legacy /api/* routes were retired in the SvelteKit/Supabase cutover. Use page actions instead.' - }, - { status: 410 } - ); - -export const GET: RequestHandler = retired; -export const POST: RequestHandler = retired; -export const PATCH: RequestHandler = retired; -export const PUT: RequestHandler = retired; -export const DELETE: RequestHandler = retired; diff --git a/src/routes/health/+server.ts b/src/routes/health/+server.js similarity index 100% rename from src/routes/health/+server.ts rename to src/routes/health/+server.js diff --git a/src/routes/invocations/+server.ts b/src/routes/invocations/+server.js similarity index 73% rename from src/routes/invocations/+server.ts rename to src/routes/invocations/+server.js index c4e26f3..6649870 100644 --- a/src/routes/invocations/+server.ts +++ b/src/routes/invocations/+server.js @@ -1,7 +1,9 @@ -import { json, type RequestHandler } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; -export const POST: RequestHandler = async ({ request }) => { - let payload: { message?: unknown } = {}; +/** @type {import('./$types').RequestHandler} */ +export const POST = async ({ request }) => { + /** @type {{ message?: unknown }} */ + let payload = {}; try { payload = await request.json(); diff --git a/src/routes/phienchotrenmay/+page.server.ts b/src/routes/phienchotrenmay/+page.server.js similarity index 91% rename from src/routes/phienchotrenmay/+page.server.ts rename to src/routes/phienchotrenmay/+page.server.js index 7e3e3b6..f3312ae 100644 --- a/src/routes/phienchotrenmay/+page.server.ts +++ b/src/routes/phienchotrenmay/+page.server.js @@ -1,5 +1,4 @@ import { fail } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; import { requireVerifiedUserForAction } from '$lib/server/auth/session'; import { assertSameOrigin } from '$lib/server/security/origin-check'; import { @@ -9,11 +8,16 @@ import { } from '$lib/server/repositories/marketplace'; import { uploadImageFromForm } from '$lib/server/storage/image-storage'; -function readText(formData: FormData, key: string) { +/** + * @param {FormData} formData + * @param {string} key + */ +function readText(formData, key) { return String(formData.get(key) ?? '').trim(); } -export const load: PageServerLoad = async (event) => { +/** @type {import('./$types').PageServerLoad} */ +export const load = async (event) => { const query = event.url.searchParams.get('q')?.trim() ?? ''; return { ...(await listMarketplace(event, event.locals.user, query)), @@ -22,7 +26,8 @@ export const load: PageServerLoad = async (event) => { }; }; -export const actions: Actions = { +/** @type {import('./$types').Actions} */ +export const actions = { createListing: async (event) => { assertSameOrigin(event); const authFailure = requireVerifiedUserForAction(event.locals.user); diff --git a/src/routes/phienchotrenmay/+page.svelte b/src/routes/phienchotrenmay/+page.svelte index 0c738cf..fa550d9 100644 --- a/src/routes/phienchotrenmay/+page.svelte +++ b/src/routes/phienchotrenmay/+page.svelte @@ -1,8 +1,8 @@ - diff --git a/src/routes/phienchotrenmay/admin/+page.server.ts b/src/routes/phienchotrenmay/admin/+page.server.js similarity index 62% rename from src/routes/phienchotrenmay/admin/+page.server.ts rename to src/routes/phienchotrenmay/admin/+page.server.js index d19b7de..f0c6f44 100644 --- a/src/routes/phienchotrenmay/admin/+page.server.ts +++ b/src/routes/phienchotrenmay/admin/+page.server.js @@ -1,28 +1,35 @@ import { fail } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; import { requireAdmin } from '$lib/server/auth/admin-roles'; import { assertSameOrigin } from '$lib/server/security/origin-check'; import { listMarketplaceAdmin, moderateMarketplaceListing } from '$lib/server/repositories/marketplace'; -import type { ListingStatus } from '$lib/shared/types/domain'; -const allowedStatuses = new Set(['pending', 'approved', 'rejected', 'hidden', 'passed']); +/** @typedef {import('$lib/shared/types/domain').ListingStatus} ListingStatus */ -function readText(formData: FormData, key: string) { +/** @type {Set} */ +const allowedStatuses = new Set(['pending', 'approved', 'rejected', 'hidden', 'passed']); + +/** + * @param {FormData} formData + * @param {string} key + */ +function readText(formData, key) { return String(formData.get(key) ?? '').trim(); } -export const load: PageServerLoad = async (event) => { +/** @type {import('./$types').PageServerLoad} */ +export const load = async (event) => { await requireAdmin(event, 'phienchotrenmay'); return listMarketplaceAdmin(event); }; -export const actions: Actions = { +/** @type {import('./$types').Actions} */ +export const actions = { setStatus: async (event) => { assertSameOrigin(event); const actor = await requireAdmin(event, 'phienchotrenmay'); const formData = await event.request.formData(); const id = readText(formData, 'id'); - const status = readText(formData, 'status') as ListingStatus; + const status = /** @type {ListingStatus} */ (readText(formData, 'status')); if (!id || !allowedStatuses.has(status)) return fail(400, { message: 'Vật phẩm admin không hợp lệ.' }); await moderateMarketplaceListing(event, actor, id, status); diff --git a/src/routes/phienchotrenmay/admin/+page.svelte b/src/routes/phienchotrenmay/admin/+page.svelte index 7f85190..56b7d54 100644 --- a/src/routes/phienchotrenmay/admin/+page.svelte +++ b/src/routes/phienchotrenmay/admin/+page.svelte @@ -1,8 +1,8 @@ - diff --git a/src/routes/vutrudodac/+page.server.ts b/src/routes/vutrudodac/+page.server.js similarity index 92% rename from src/routes/vutrudodac/+page.server.ts rename to src/routes/vutrudodac/+page.server.js index 73211c7..b7ccd79 100644 --- a/src/routes/vutrudodac/+page.server.ts +++ b/src/routes/vutrudodac/+page.server.js @@ -1,29 +1,35 @@ import { fail } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; import { requireVerifiedUserForAction } from '$lib/server/auth/session'; import { assertSameOrigin } from '$lib/server/security/origin-check'; import { createFoundReport, createLostReport, listLostFoundDashboard } from '$lib/server/repositories/lost-found'; import { markNotificationRead } from '$lib/server/repositories/notifications'; import { uploadImageFromForm } from '$lib/server/storage/image-storage'; -function readText(formData: FormData, key: string) { +/** + * @param {FormData} formData + * @param {string} key + */ +function readText(formData, key) { return String(formData.get(key) ?? '').trim(); } -function validateDescription(description: string) { +/** @param {string} description */ +function validateDescription(description) { if (description.length < 3) return 'Mô tả cần rõ hơn để radar dò đúng tín hiệu.'; if (description.length > 2000) return 'Mô tả đang quá dài cho một tín hiệu Artemis.'; return null; } -export const load: PageServerLoad = async (event) => { +/** @type {import('./$types').PageServerLoad} */ +export const load = async (event) => { return { ...(await listLostFoundDashboard(event, event.locals.user)), user: event.locals.user }; }; -export const actions: Actions = { +/** @type {import('./$types').Actions} */ +export const actions = { createLost: async (event) => { assertSameOrigin(event); const authFailure = requireVerifiedUserForAction(event.locals.user); diff --git a/src/routes/vutrudodac/+page.svelte b/src/routes/vutrudodac/+page.svelte index b0f6ef4..4d368f8 100644 --- a/src/routes/vutrudodac/+page.svelte +++ b/src/routes/vutrudodac/+page.svelte @@ -1,8 +1,8 @@ - diff --git a/src/routes/vutrudodac/admin/+page.server.ts b/src/routes/vutrudodac/admin/+page.server.js similarity index 64% rename from src/routes/vutrudodac/admin/+page.server.ts rename to src/routes/vutrudodac/admin/+page.server.js index a1e3349..5e11df5 100644 --- a/src/routes/vutrudodac/admin/+page.server.ts +++ b/src/routes/vutrudodac/admin/+page.server.js @@ -1,29 +1,36 @@ import { fail } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; import { requireAdmin } from '$lib/server/auth/admin-roles'; import { assertSameOrigin } from '$lib/server/security/origin-check'; import { listLostFoundAdmin, updateReportStatus } from '$lib/server/repositories/lost-found'; -import type { ReportStatus } from '$lib/shared/types/domain'; -const allowedStatuses = new Set(['open', 'matched', 'returned', 'closed', 'hidden']); +/** @typedef {import('$lib/shared/types/domain').ReportStatus} ReportStatus */ -function readText(formData: FormData, key: string) { +/** @type {Set} */ +const allowedStatuses = new Set(['open', 'matched', 'returned', 'closed', 'hidden']); + +/** + * @param {FormData} formData + * @param {string} key + */ +function readText(formData, key) { return String(formData.get(key) ?? '').trim(); } -export const load: PageServerLoad = async (event) => { +/** @type {import('./$types').PageServerLoad} */ +export const load = async (event) => { await requireAdmin(event, 'vutrudodac'); return listLostFoundAdmin(event); }; -export const actions: Actions = { +/** @type {import('./$types').Actions} */ +export const actions = { setStatus: async (event) => { assertSameOrigin(event); const actor = await requireAdmin(event, 'vutrudodac'); const formData = await event.request.formData(); const id = readText(formData, 'id'); const kind = readText(formData, 'kind'); - const status = readText(formData, 'status') as ReportStatus; + const status = /** @type {ReportStatus} */ (readText(formData, 'status')); if (!id || (kind !== 'lost' && kind !== 'found') || !allowedStatuses.has(status)) { return fail(400, { message: 'Tín hiệu admin không hợp lệ.' }); diff --git a/src/routes/vutrudodac/admin/+page.svelte b/src/routes/vutrudodac/admin/+page.svelte index ad1eba8..9394169 100644 --- a/src/routes/vutrudodac/admin/+page.svelte +++ b/src/routes/vutrudodac/admin/+page.svelte @@ -1,8 +1,8 @@ - diff --git a/vite.config.ts b/vite.config.js similarity index 100% rename from vite.config.ts rename to vite.config.js