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 };
  }
}