Spaces:
Sleeping
Sleeping
File size: 5,013 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import {
Body,
Controller,
Delete,
Get,
Header,
HttpCode,
HttpException,
Param,
Post,
Put,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import type { Response } from 'express';
import type { RegionGeo } from '@trek/shared';
import type { User } from '../../types';
import { AtlasService } from './atlas.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
/**
* /api/addons/atlas — visited countries/regions, region GeoJSON, bucket list.
*
* Byte-identical to the legacy Express route (server/src/routes/atlas.ts): all
* endpoints require auth; country/region codes are upper-cased; /regions is
* always no-store while /regions/geo is cached for a day only on a non-empty
* result; the mark POSTs answer 200 (not Nest's default 201); and the bespoke
* 400/404 bodies are reproduced exactly. No addon gate — the legacy route has
* none, so adding one would break clients when the addon is off.
*/
@Controller('api/addons/atlas')
@UseGuards(JwtAuthGuard)
export class AtlasController {
constructor(private readonly atlas: AtlasService) {}
@Get('stats')
stats(@CurrentUser() user: User) {
return this.atlas.stats(user.id);
}
@Get('regions')
@Header('Cache-Control', 'no-cache, no-store')
regions(@CurrentUser() user: User) {
return this.atlas.visitedRegions(user.id);
}
@Get('regions/geo')
async regionGeo(
@Query('countries') countries: string | undefined,
@Res({ passthrough: true }) res: Response,
): Promise<RegionGeo> {
const list = (countries || '').split(',').filter(Boolean);
if (list.length === 0) {
return { type: 'FeatureCollection', features: [] };
}
const geo = await this.atlas.regionGeo(list);
// Cache only a non-empty result, matching the legacy route (the empty
// short-circuit above sends no Cache-Control header).
res.setHeader('Cache-Control', 'public, max-age=86400');
return geo;
}
@Get('countries/geo')
@Header('Cache-Control', 'public, max-age=86400')
countryGeo(): RegionGeo {
return this.atlas.countryGeo();
}
@Get('country/:code')
countryPlaces(@CurrentUser() user: User, @Param('code') code: string) {
return this.atlas.countryPlaces(user.id, code.toUpperCase());
}
@Post('country/:code/mark')
@HttpCode(200)
markCountry(@CurrentUser() user: User, @Param('code') code: string): { success: boolean } {
this.atlas.markCountry(user.id, code.toUpperCase());
return { success: true };
}
@Delete('country/:code/mark')
unmarkCountry(@CurrentUser() user: User, @Param('code') code: string): { success: boolean } {
this.atlas.unmarkCountry(user.id, code.toUpperCase());
return { success: true };
}
@Post('region/:code/mark')
@HttpCode(200)
markRegion(
@CurrentUser() user: User,
@Param('code') code: string,
@Body('name') name?: string,
@Body('country_code') countryCode?: string,
): { success: boolean } {
if (!name || !countryCode) {
throw new HttpException({ error: 'name and country_code are required' }, 400);
}
this.atlas.markRegion(user.id, code.toUpperCase(), name, countryCode.toUpperCase());
return { success: true };
}
@Delete('region/:code/mark')
unmarkRegion(@CurrentUser() user: User, @Param('code') code: string): { success: boolean } {
this.atlas.unmarkRegion(user.id, code.toUpperCase());
return { success: true };
}
@Get('bucket-list')
bucketList(@CurrentUser() user: User) {
return { items: this.atlas.bucketList(user.id) };
}
@Post('bucket-list')
createBucketItem(
@CurrentUser() user: User,
@Body() body: { name?: string; lat?: number | null; lng?: number | null; country_code?: string | null; notes?: string | null; target_date?: string | null },
): { item: unknown } {
if (!body.name?.trim()) {
throw new HttpException({ error: 'Name is required' }, 400);
}
const { name, lat, lng, country_code, notes, target_date } = body;
return { item: this.atlas.createBucketItem(user.id, { name, lat, lng, country_code, notes, target_date }) };
}
@Put('bucket-list/:id')
updateBucketItem(
@CurrentUser() user: User,
@Param('id') id: string,
@Body() body: { name?: string; notes?: string; lat?: number | null; lng?: number | null; country_code?: string | null; target_date?: string | null },
): { item: unknown } {
const { name, notes, lat, lng, country_code, target_date } = body;
const item = this.atlas.updateBucketItem(user.id, id, { name, notes, lat, lng, country_code, target_date });
if (!item) {
throw new HttpException({ error: 'Item not found' }, 404);
}
return { item };
}
@Delete('bucket-list/:id')
deleteBucketItem(@CurrentUser() user: User, @Param('id') id: string): { success: boolean } {
if (!this.atlas.deleteBucketItem(user.id, id)) {
throw new HttpException({ error: 'Item not found' }, 404);
}
return { success: true };
}
}
|