Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,601 changes: 10,601 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 5 additions & 11 deletions src/air/air.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AirController } from './air.controller';
import { AirService } from './air.service';
import { SupabaseService } from '../supabase/supabase.service';
import { TimezoneFormatterService } from '../common/timezone-formatter.service';
import { createTestingModuleWithCommonProviders } from '../common/test-helpers';

describe('AirController', () => {
let controller: AirController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AirController],
providers: [
AirService,
{ provide: SupabaseService, useValue: {} },
TimezoneFormatterService,
],
}).compile();
const module = await createTestingModuleWithCommonProviders(
[AirService],
[AirController],
);

controller = module.get<AirController>(AirController);
});
Expand Down
54 changes: 46 additions & 8 deletions src/air/air.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { BadRequestException, Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
import {
BadRequestException,
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
UseGuards,
} from '@nestjs/common';
import { AirService } from './air.service';
import { AirDataDto } from './dto/air-data.dto';
import { CreateAirDto } from './dto/create-air.dto';
import { JwtAuthGuard } from '../auth/guards/jwt.auth.guard';
import { ApiBadRequestResponse, ApiBearerAuth, ApiInternalServerErrorResponse, ApiOkResponse, ApiParam, ApiQuery, ApiSecurity, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiParam,
ApiQuery,
ApiSecurity,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { ErrorResponseDto } from '../common/dto/error-response.dto';

@Controller('air')
Expand All @@ -24,28 +44,47 @@ export class AirController {

@Get(':dev_eui')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ description: 'Air data returned successfully.', type: AirDataDto, isArray: true })
@ApiOkResponse({
description: 'Air data returned successfully.',
type: AirDataDto,
isArray: true,
})
@ApiBadRequestResponse({
description: 'Invalid dev_eui, start/end, or timezone.',
type: ErrorResponseDto,
example: { statusCode: 400, error: 'Bad Request', message: 'Validation failed' },
example: {
statusCode: 400,
error: 'Bad Request',
message: 'Validation failed',
},
})
@ApiUnauthorizedResponse({
description: 'Missing or invalid bearer token.',
type: ErrorResponseDto,
example: { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' },
example: {
statusCode: 401,
error: 'Unauthorized',
message: 'Unauthorized',
},
})
@ApiInternalServerErrorResponse({
description: 'Failed to fetch air data.',
type: ErrorResponseDto,
example: { statusCode: 500, error: 'Internal Server Error', message: 'Failed to fetch air data' },
example: {
statusCode: 500,
error: 'Internal Server Error',
message: 'Failed to fetch air data',
},
})
@ApiParam({ name: 'dev_eui', description: 'Device dev_eui' })
@ApiQuery({
name: 'start',
required: false,
description: 'ISO 8601 date/time. Defaults to now (page loaded time).',
schema: { type: 'string', default: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() }, // SHOULD be the date in ISO 8601, minus 24 hours
schema: {
type: 'string',
default: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
}, // SHOULD be the date in ISO 8601, minus 24 hours
example: '2024-01-01T00:00:00Z',
})
@ApiQuery({
Expand Down Expand Up @@ -89,5 +128,4 @@ export class AirController {

return this.airService.findOne(devEui, startDate, endDate, timezone);
}

}
12 changes: 2 additions & 10 deletions src/air/air.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AirService } from './air.service';
import { SupabaseService } from '../supabase/supabase.service';
import { TimezoneFormatterService } from '../common/timezone-formatter.service';
import { createTestingModuleWithCommonProviders } from '../common/test-helpers';

describe('AirService', () => {
let service: AirService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AirService,
{ provide: SupabaseService, useValue: {} },
TimezoneFormatterService,
],
}).compile();
const module = await createTestingModuleWithCommonProviders([AirService]);

service = module.get<AirService>(AirService);
});
Expand Down
52 changes: 7 additions & 45 deletions src/air/air.service.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,14 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { SupabaseService } from '../supabase/supabase.service';
import { TableRow } from '../types/supabase';
// import { CreateAirDto } from './dto/create-air.dto';
// import { UpdateAirDto } from './dto/update-air.dto';
import { TimezoneFormatterService } from '../common/timezone-formatter.service';
import { BaseDataService } from '../common/base-data.service';

@Injectable()
export class AirService {
export class AirService extends BaseDataService<'cw_air_data'> {
constructor(
private readonly supabaseService: SupabaseService,
private readonly timezoneFormatter: TimezoneFormatterService,
) {}

// create(createAirDto: CreateAirDto) {
// return 'This action adds a new air';
// }

// findAll() {
// return `This action returns all air`;
// }

async findOne(
devEui: string,
startDate: Date,
endDate: Date,
timezone?: string,
): Promise<TableRow<'cw_air_data'>[]> {
const normalizedTimeZone = timezone?.trim() || null;
if (normalizedTimeZone) {
this.timezoneFormatter.assertValidTimeZone(normalizedTimeZone);
}

const { data, error } = await this.supabaseService
.getClient()
.from('cw_air_data')
.select('*')
.eq('dev_eui', devEui)
.gte('created_at', startDate.toISOString())
.lte('created_at', endDate.toISOString())
.order('created_at', { ascending: true });

if (error) {
throw new InternalServerErrorException('Failed to fetch air data');
}

return (data ?? []).map((row) => ({
...row,
created_at: this.timezoneFormatter.formatTimestamp(row.created_at, normalizedTimeZone),
}));
supabaseService: SupabaseService,
timezoneFormatter: TimezoneFormatterService,
) {
super(supabaseService, timezoneFormatter, 'cw_air_data');
}
}
4 changes: 3 additions & 1 deletion src/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('AppController', () => {
it('should serve the API homepage (index.html)', () => {
const sendFile = jest.fn();
appController.getHello({ sendFile } as any);
expect(sendFile).toHaveBeenCalledWith(join(process.cwd(), 'static', 'index.html'));
expect(sendFile).toHaveBeenCalledWith(
join(process.cwd(), 'static', 'index.html'),
);
});
});
});
9 changes: 3 additions & 6 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,11 @@ import { DevicesModule } from './devices/devices.module';
ttl: 2000,
limit: 2,
blockDuration: 5000,
}
},
]),
DevicesModule,
],
controllers: [AppController],
providers: [
AppService,
{ provide: APP_GUARD, useClass: ThrottlerGuard },
],
providers: [AppService, { provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class AppModule { }
export class AppModule {}
122 changes: 75 additions & 47 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt.auth.guard';
import { ApiBadRequestResponse, ApiBearerAuth, ApiBody, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiSecurity, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { ErrorResponseDto } from '../common/dto/error-response.dto';
import { LoginResponseDto } from './dto/login-response.dto';
Expand All @@ -9,52 +18,71 @@ import { LoginResponseDto } from './dto/login-response.dto';
@ApiBearerAuth('bearerAuth')
@ApiSecurity('apiKey')
export class AuthController {
constructor(private readonly authService: AuthService) {}
constructor(private readonly authService: AuthService) {}

@Get()
@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description: 'Authenticated user returned successfully.',
schema: { type: 'object', additionalProperties: true },
})
@ApiUnauthorizedResponse({
description: 'Missing or invalid bearer token.',
type: ErrorResponseDto,
example: { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' },
})
async protected(@Req() req) {
return req.user;
}
@Get()
@UseGuards(JwtAuthGuard)
@ApiOkResponse({
description: 'Authenticated user returned successfully.',
schema: { type: 'object', additionalProperties: true },
})
@ApiUnauthorizedResponse({
description: 'Missing or invalid bearer token.',
type: ErrorResponseDto,
example: {
statusCode: 401,
error: 'Unauthorized',
message: 'Unauthorized',
},
})
async protected(@Req() req) {
return req.user;
}

@Post('login')
@ApiOperation({ summary: 'Login with email and password' })
@ApiOkResponse({ description: 'Login successful. Returns access token and user data.', type: LoginResponseDto })
@ApiBadRequestResponse({
description: 'Invalid login payload.',
type: ErrorResponseDto,
example: { statusCode: 400, error: 'Bad Request', message: 'Validation failed' },
})
@ApiUnauthorizedResponse({
description: 'Invalid email or password.',
type: ErrorResponseDto,
example: { statusCode: 401, error: 'Unauthorized', message: 'Invalid email or password' },
})
@ApiInternalServerErrorResponse({
description: 'Failed to login.',
type: ErrorResponseDto,
example: { statusCode: 500, error: 'Internal Server Error', message: 'Failed to login' },
})
@ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', example: 'user@example.com' },
password: { type: 'string', example: 'StrongPassword123!' },
},
required: ['email', 'password'],
},
})
async login(@Body() body: { email: string; password: string }) {
return this.authService.loginWithPassword(body.email, body.password);
}
@Post('login')
@ApiOperation({ summary: 'Login with email and password' })
@ApiOkResponse({
description: 'Login successful. Returns access token and user data.',
type: LoginResponseDto,
})
@ApiBadRequestResponse({
description: 'Invalid login payload.',
type: ErrorResponseDto,
example: {
statusCode: 400,
error: 'Bad Request',
message: 'Validation failed',
},
})
@ApiUnauthorizedResponse({
description: 'Invalid email or password.',
type: ErrorResponseDto,
example: {
statusCode: 401,
error: 'Unauthorized',
message: 'Invalid email or password',
},
})
@ApiInternalServerErrorResponse({
description: 'Failed to login.',
type: ErrorResponseDto,
example: {
statusCode: 500,
error: 'Internal Server Error',
message: 'Failed to login',
},
})
@ApiBody({
schema: {
type: 'object',
properties: {
email: { type: 'string', example: 'user@example.com' },
password: { type: 'string', example: 'StrongPassword123!' },
},
required: ['email', 'password'],
},
})
async login(@Body() body: { email: string; password: string }) {
return this.authService.loginWithPassword(body.email, body.password);
}
}
Loading