initial: Backend Auth Module + Design System + Complete Documentation

- Setup NestJS with TypeScript, ConfigModule, JWT authentication
- Implemented Auth Module with signup, login, logout endpoints
- Created DTOs with validation (SignupDto, LoginDto)
- JWT Strategy with Passport integration for token validation
- JwtAuthGuard for route protection with Bearer tokens
- CurrentUser decorator for dependency injection
- Supabase integration for user management and auth
- Complete API documentation (API.md) with all endpoints
- Design System for Web (Next.js + Tailwind) and Mobile (Flutter)
- Comprehensive project documentation and roadmap
- Environment configuration with Joi schema validation
- Ready for Tasks Module and RLS implementation
This commit is contained in:
Erik Silva
2025-12-01 01:17:00 -03:00
commit 35272b8f87
56 changed files with 20691 additions and 0 deletions

29
backend-api/.env.example Normal file
View File

@@ -0,0 +1,29 @@
# ======================================
# SUPABASE CONFIGURATION
# ======================================
SUPABASE_URL=https://supabase.stackbackup.cloud
SUPABASE_ANON_KEY=your_supabase_anon_key_here
SUPABASE_SERVICE_KEY=your_supabase_service_key_here
# ======================================
# JWT CONFIGURATION
# ======================================
JWT_SECRET=your_jwt_secret_key_here_min_32_chars_long
JWT_EXPIRATION=7d
# ======================================
# DATABASE CONFIGURATION
# ======================================
DATABASE_URL=postgresql://user:password@localhost:5432/taskmanager
# ======================================
# APP CONFIGURATION
# ======================================
NODE_ENV=development
PORT=3000
API_PREFIX=/api
# ======================================
# CORS CONFIGURATION
# ======================================
CORS_ORIGIN=http://localhost:3000,http://localhost:3001

56
backend-api/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# 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

4
backend-api/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

359
backend-api/API.md Normal file
View File

@@ -0,0 +1,359 @@
# 🔐 TASK MANAGER - API Backend Documentation
## 📌 Base URL
```
http://localhost:3000/api
```
## 🔑 Autenticação
Todas as requisições protegidas devem incluir o header:
```
Authorization: Bearer {token}
```
---
## 📋 Endpoints da API
### 1. Autenticação (Auth)
#### 1.1 Registrar (Signup)
```http
POST /auth/signup
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword123",
"name": "João Silva"
}
```
**Response (201 Created):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"email_confirmed_at": "2025-12-01T10:00:00Z"
}
}
```
**Erros:**
- `400 Bad Request` - Dados inválidos
- `409 Conflict` - Email já registrado
---
#### 1.2 Login
```http
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword123"
}
```
**Response (200 OK):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"email_confirmed_at": "2025-12-01T10:00:00Z"
}
}
```
**Erros:**
- `401 Unauthorized` - Email ou senha incorretos
- `400 Bad Request` - Dados inválidos
---
#### 1.3 Logout
```http
POST /auth/logout
Authorization: Bearer {token}
```
**Response (200 OK):**
```json
{
"message": "Logout realizado com sucesso"
}
```
**Erros:**
- `401 Unauthorized` - Token inválido ou expirado
---
#### 1.4 Obter Perfil Atual
```http
GET /auth/me
Authorization: Bearer {token}
```
**Response (200 OK):**
```json
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"iat": 1701427200,
"exp": 1702032000
}
```
**Erros:**
- `401 Unauthorized` - Token inválido ou expirado
---
#### 1.5 Recuperar Senha
```http
POST /auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}
```
**Response (200 OK):**
```json
{
"message": "Email de recuperação enviado. Verifique sua caixa de entrada."
}
```
---
### 2. Tarefas (Tasks) - Em Desenvolvimento
#### 2.1 Listar Tarefas
```http
GET /tasks
Authorization: Bearer {token}
# Query params opcionais:
?status=all|completed|pending
?sort=created_at|updated_at
?order=asc|desc
```
**Response (200 OK):**
```json
{
"data": [
{
"id": "6b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Fazer compras",
"description": "Ir ao supermercado",
"completed": false,
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-01T10:00:00Z"
}
],
"total": 1,
"page": 1
}
```
---
#### 2.2 Obter Tarefa Específica
```http
GET /tasks/:id
Authorization: Bearer {token}
```
**Response (200 OK):**
```json
{
"id": "6b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Fazer compras",
"description": "Ir ao supermercado",
"completed": false,
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-01T10:00:00Z"
}
```
**Erros:**
- `404 Not Found` - Tarefa não encontrada
- `401 Unauthorized` - Token inválido
---
#### 2.3 Criar Tarefa
```http
POST /tasks
Authorization: Bearer {token}
Content-Type: application/json
{
"title": "Fazer compras",
"description": "Ir ao supermercado"
}
```
**Response (201 Created):**
```json
{
"id": "6b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Fazer compras",
"description": "Ir ao supermercado",
"completed": false,
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-01T10:00:00Z"
}
```
**Erros:**
- `400 Bad Request` - Título obrigatório
- `401 Unauthorized` - Token inválido
---
#### 2.4 Atualizar Tarefa
```http
PATCH /tasks/:id
Authorization: Bearer {token}
Content-Type: application/json
{
"title": "Fazer compras (atualizado)",
"description": "Ir ao supermercado e padaria",
"completed": true
}
```
**Response (200 OK):**
```json
{
"id": "6b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Fazer compras (atualizado)",
"description": "Ir ao supermercado e padaria",
"completed": true,
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-01T10:30:00Z"
}
```
**Erros:**
- `404 Not Found` - Tarefa não encontrada
- `400 Bad Request` - Dados inválidos
- `401 Unauthorized` - Token inválido
---
#### 2.5 Deletar Tarefa
```http
DELETE /tasks/:id
Authorization: Bearer {token}
```
**Response (200 OK):**
```json
{
"message": "Tarefa deletada com sucesso"
}
```
**Erros:**
- `404 Not Found` - Tarefa não encontrada
- `401 Unauthorized` - Token inválido
---
## 🔄 Real-time (WebSocket)
### Conectar ao Realtime
```javascript
const subscription = supabase
.channel('tasks')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'tasks' },
(payload) => console.log(payload)
)
.subscribe();
```
### Eventos
- `INSERT` - Nova tarefa criada
- `UPDATE` - Tarefa atualizada
- `DELETE` - Tarefa deletada
---
## ⚠️ Códigos de Erro
| Código | Significado |
|--------|-------------|
| `200` | OK - Requisição bem-sucedida |
| `201` | Created - Recurso criado |
| `400` | Bad Request - Dados inválidos |
| `401` | Unauthorized - Token inválido/expirado |
| `404` | Not Found - Recurso não encontrado |
| `409` | Conflict - Recurso já existe |
| `500` | Internal Server Error - Erro do servidor |
---
## 🛠️ Exemplo Completo (cURL)
### 1. Registrar
```bash
curl -X POST http://localhost:3000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"name": "João"
}'
```
### 2. Login
```bash
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'
```
### 3. Criar Tarefa (com token)
```bash
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d '{
"title": "Fazer compras",
"description": "Ir ao supermercado"
}'
```
---
## 📚 Referências
- **Supabase Docs**: https://supabase.com/docs
- **NestJS Docs**: https://docs.nestjs.com
- **JWT**: https://jwt.io
---
**API Status**: ✅ Pronta para Desenvolvimento
**Última Atualização**: 1 de dezembro de 2025

98
backend-api/README.md Normal file
View File

@@ -0,0 +1,98 @@
<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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).

View File

@@ -0,0 +1,35 @@
// @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" }],
},
},
);

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

10381
backend-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

81
backend-api/package.json Normal file
View File

@@ -0,0 +1,81 @@
{
"name": "backend-api",
"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": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@supabase/supabase-js": "^2.86.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"dotenv": "^17.2.3",
"joi": "^18.0.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"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": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.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"
}
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -0,0 +1,48 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import appConfig from './config/app.config';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import { SupabaseService } from './config/supabase.service';
import { JwtStrategy } from './auth/strategies/jwt.strategy';
import { AuthModule } from './auth/auth.module';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig, databaseConfig, jwtConfig],
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
SUPABASE_URL: Joi.string().required(),
SUPABASE_ANON_KEY: Joi.string().required(),
SUPABASE_SERVICE_KEY: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
}),
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('jwt.secret'),
signOptions: {
expiresIn: '7d' as const,
},
}),
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService, SupabaseService, JwtStrategy],
exports: [SupabaseService, JwtModule],
})
export class AppModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,75 @@
import {
Controller,
Post,
Body,
Get,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
import { JwtAuthGuard } from './guards/jwt.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
/**
* POST /auth/signup
* Registrar novo usuário
*/
@Post('signup')
@HttpCode(HttpStatus.CREATED)
async signup(@Body() signupDto: SignupDto) {
return this.authService.signup(signupDto);
}
/**
* POST /auth/login
* Fazer login
*/
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
/**
* POST /auth/logout
* Fazer logout
*/
@Post('logout')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
async logout(@CurrentUser() user: any) {
return this.authService.logout(user.userId);
}
/**
* GET /auth/me
* Obter dados do usuário autenticado
*/
@Get('me')
@UseGuards(JwtAuthGuard)
async getProfile(@CurrentUser() user: any) {
return {
userId: user.userId,
email: user.email,
iat: user.iat,
exp: user.exp,
};
}
/**
* POST /auth/forgot-password
* Solicitar reset de senha
*/
@Post('forgot-password')
@HttpCode(HttpStatus.OK)
async forgotPassword(@Body('email') email: string) {
return this.authService.requestPasswordReset(email);
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,145 @@
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { SupabaseService } from '../config/supabase.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
import * as crypto from 'crypto';
@Injectable()
export class AuthService {
constructor(
private readonly supabaseService: SupabaseService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
/**
* Registrar novo usuário
*/
async signup(signupDto: SignupDto) {
try {
// Criar usuário no Supabase Auth
const user = await this.supabaseService.createUser(
signupDto.email,
signupDto.password,
);
// Criar registro do usuário na tabela users (opcional)
// await this.usersService.create({
// id: user.id,
// email: user.email,
// name: signupDto.name,
// });
// Gerar token JWT
const token = this.generateToken(user.id, user.email);
return {
access_token: token,
user: {
id: user.id,
email: user.email,
email_confirmed_at: user.email_confirmed_at,
},
};
} catch (error) {
if (error.message?.includes('already registered')) {
throw new ConflictException('Email já está registrado');
}
throw error;
}
}
/**
* Fazer login
*/
async login(loginDto: LoginDto) {
try {
const supabase = this.supabaseService.getClient();
const { data, error } = await supabase.auth.signInWithPassword({
email: loginDto.email,
password: loginDto.password,
});
if (error) {
throw new UnauthorizedException('Email ou senha incorretos');
}
// Gerar token JWT customizado
const token = this.generateToken(data.user.id, data.user.email);
return {
access_token: token,
user: {
id: data.user.id,
email: data.user.email,
email_confirmed_at: data.user.email_confirmed_at,
},
};
} catch (error) {
throw new UnauthorizedException('Email ou senha incorretos');
}
}
/**
* Fazer logout (validação do token)
*/
async logout(userId: string) {
// Em um sistema real, você poderia adicionar o token a uma blacklist
// Por enquanto, apenas validamos que o usuário tem um token válido
return { message: 'Logout realizado com sucesso' };
}
/**
* Validar token e retornar dados do usuário
*/
async validateToken(token: string) {
try {
const decoded = this.jwtService.verify(token);
return decoded;
} catch (error) {
throw new UnauthorizedException('Token inválido ou expirado');
}
}
/**
* Gerar token JWT
*/
private generateToken(userId: string, email: string): string {
const payload = {
sub: userId,
email: email,
iat: Math.floor(Date.now() / 1000),
};
return this.jwtService.sign(payload);
}
/**
* Recuperar senha (envia email com link de reset)
*/
async requestPasswordReset(email: string) {
try {
const supabase = this.supabaseService.getClient();
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/reset-password`,
});
if (error) {
throw error;
}
return {
message: 'Email de recuperação enviado. Verifique sua caixa de entrada.',
};
} catch (error) {
// Não revelar se o email existe ou não por segurança
return {
message: 'Se o email existir, você receberá um link de recuperação.',
};
}
}
}

View File

@@ -0,0 +1,10 @@
import { IsEmail, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}

View File

@@ -0,0 +1,13 @@
import { IsEmail, IsString, MinLength } from 'class-validator';
export class SignupDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsString()
name?: string;
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('jwt.secret'),
});
}
async validate(payload: any) {
return {
userId: payload.sub,
email: payload.email,
iat: payload.iat,
exp: payload.exp,
};
}
}

View File

@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

@@ -0,0 +1,11 @@
import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3000', 10),
apiPrefix: process.env.API_PREFIX || '/api',
cors: {
origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
credentials: true,
},
}));

View File

@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
supabaseUrl: process.env.SUPABASE_URL,
supabaseAnonKey: process.env.SUPABASE_ANON_KEY,
supabaseServiceKey: process.env.SUPABASE_SERVICE_KEY,
databaseUrl: process.env.DATABASE_URL,
}));

View File

@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRATION || '7d',
}));

View File

@@ -0,0 +1,40 @@
import { createClient } from '@supabase/supabase-js';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class SupabaseService {
private supabaseClient;
constructor(private configService: ConfigService) {
const supabaseUrl = this.configService.get<string>('database.supabaseUrl');
const supabaseKey = this.configService.get<string>('database.supabaseServiceKey');
if (!supabaseUrl || !supabaseKey) {
throw new Error('SUPABASE_URL and SUPABASE_SERVICE_KEY must be defined');
}
this.supabaseClient = createClient(supabaseUrl, supabaseKey);
}
getClient() {
return this.supabaseClient;
}
// Helper para criar usuario
async createUser(email: string, password: string) {
const { data, error } = await this.supabaseClient.auth.admin.createUser({
email,
password,
email_confirm: true,
});
if (error) throw error;
return data.user;
}
// Helper para fazer queries
async query(table: string, method: 'select' | 'insert' | 'update' | 'delete' = 'select') {
return this.supabaseClient.from(table)[method]();
}
}

32
backend-api/src/main.ts Normal file
View File

@@ -0,0 +1,32 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Validação global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// CORS
app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
credentials: true,
});
// API Prefix
const port = process.env.PORT ?? 3000;
const apiPrefix = process.env.API_PREFIX ?? '/api';
app.setGlobalPrefix(apiPrefix);
await app.listen(port, '0.0.0.0', () => {
console.log(`🚀 Server running on http://localhost:${port}${apiPrefix}`);
});
}
bootstrap();

View File

@@ -0,0 +1,9 @@
import { IsString, IsEmail } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
name?: string;
}

View File

@@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
backend-api/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}