initial commit

This commit is contained in:
2024-07-19 19:55:57 +02:00
commit 049e063d48
53 changed files with 15239 additions and 0 deletions

25
back/.eslintrc.js Normal file
View File

@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

56
back/.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
back/.prettierrc Normal file
View File

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

23
back/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# Use the official Node.js slim image as a base image
FROM node:18-slim
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
# Build the NestJS application
RUN npm run build
# Expose the port that the NestJS application runs on
EXPOSE 3000
# Start the NestJS application
CMD ["npm", "run", "start:prod"]

21
back/LICENCE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Daniel Heras Quesada
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
back/README.md Normal file
View File

@@ -0,0 +1,67 @@
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript archetype repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Authentication functionality
All done with the `Auth` decorator, which lets you determine a list of authorized roles.
The are 3 available roles:
- Public: no role nor user needed
- User
- Admin
These can be extended by simply adding new elements to the `src/users/roles/role.enum.ts` enumeration.
```ts
@Auth(Role.Public)
@Get()
getHello(): string { return "Hello!" }
```
### JWT
User login is managed by a `JWT` based structure. The client must send both username and password in the body of a `POST` method at `/auth/login`, which would return a `JWT` in case of success. This token must be used to interact with the server on every non-public route. Internally the server assumes the user passwords are stored hashed on a [Mariadb](https://mariadb.org/) database.
## Stay in touch
- Author - [Daniel Heras Quesada](https://dqnid.com)
- Twitter - [@nestframework](https://twitter.com/nestframework)
- Linkedin - [daniel-heras-quesada](https://www.linkedin.com/in/daniel-heras-quesada/)
## License
This archetype is [MIT licensed](LICENSE).

10
back/docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: nestjs-app
ports:
- '3000:3000'

8
back/nest-cli.json Normal file
View File

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

9774
back/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

78
back/package.json Normal file
View File

@@ -0,0 +1,78 @@
{
"name": "back-nest-archetype",
"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": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/throttler": "^5.1.2",
"@nestjs/typeorm": "^10.0.2",
"bcrypt": "^5.1.1",
"mysql2": "^3.9.7",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"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,18 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Throttle } from '@nestjs/throttler';
import { Auth } from './auth/auth.decorator';
import { Role } from './users/roles/role.enum';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// Override default configuration for Rate limiting and duration.
@Throttle({ default: { limit: 10, ttl: 1000 } })
@Auth(Role.Public)
@Get()
getHello(): string {
return this.appService.getHello();
}
}

37
back/src/app.module.ts Normal file
View File

@@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { ThrottlerModule } from '@nestjs/throttler';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/entities/user.entity';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
AuthModule,
UsersModule,
ThrottlerModule.forRoot([
{
ttl: 10000,
limit: 20,
},
]),
TypeOrmModule.forRoot({
type: process.env.DB_TYPE as 'mysql' | 'mariadb',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_MAIN,
entities: [User],
synchronize: false,
connectTimeout: 20000,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
back/src/app.service.ts Normal file
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,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,35 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { Role } from 'src/users/roles/role.enum';
import { Auth } from './auth.decorator';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Auth(Role.Public)
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@Auth(Role.Admin)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
@Get('options')
getOptions(@Request() req) {
return req.roles;
}
}

View File

@@ -0,0 +1,8 @@
import { SetMetadata, applyDecorators } from '@nestjs/common';
import { Role } from 'src/users/roles/role.enum';
export const ROLES_METADATA = 'roles';
export function Auth(...roles: Role[]) {
return applyDecorators(SetMetadata(ROLES_METADATA, roles));
}

View File

@@ -0,0 +1,60 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
import { Reflector } from '@nestjs/core';
import { ROLES_METADATA } from './auth.decorator';
import { Role } from 'src/users/roles/role.enum';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const decorator_roles = this.reflector.getAllAndOverride<Role[]>(
ROLES_METADATA,
[context.getHandler(), context.getClass()],
);
if (decorator_roles && decorator_roles.includes(Role.Public)) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
const found = payload.roles.some((r: Role) =>
decorator_roles.includes(r),
);
if (!found) {
throw new UnauthorizedException();
}
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';
//TODO: TIMING LOGIC: ID Token: 60 minutes. Access Token: 60 minutes. Refresh Token: 90 days
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60m' },
}),
],
providers: [
AuthService,
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,32 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async signIn(
username: string,
pass: string,
): Promise<{ access_token: string }> {
console.log(username, pass);
const user = await this.usersService.findOne(username);
if (!user) throw new UnauthorizedException();
const isSamePasswd = await bcrypt.compare(`${pass}`, `${user?.password}`);
if (!isSamePasswd) throw new UnauthorizedException();
const payload = {
sub: user.id,
username: user.username,
roles: user.roles,
};
return { access_token: await this.jwtService.signAsync(payload) };
}
}

View File

@@ -0,0 +1,5 @@
//TODO: remove this and do it propertly
export const jwtConstants = {
secret:
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};

9
back/src/main.ts Normal file
View File

@@ -0,0 +1,9 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
bootstrap();

View File

@@ -0,0 +1,19 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
// TODO: username should be unique and, maybe can act as ID?
// NOTE: CREATE TABLE user (id bigint primary key DEFAULT UUID_SHORT(), username char(20) not null, password char(20) not null, roles char(50) not null);
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
@Column()
roles: string;
}

View File

@@ -0,0 +1,5 @@
export enum Role {
Public = 'public',
User = 'user',
Admin = 'admin',
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { Role } from './roles/role.enum';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { DataSource, Repository } from 'typeorm';
export type UserType = {
id: number;
username: string;
password: string;
roles: Role[];
};
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectDataSource()
private dataSource: DataSource,
) {}
async findOne(username: string): Promise<UserType | undefined> {
const db_user = await this.usersRepository.findOneBy({ username });
if (!db_user) return null;
//TODO: change this shabby mapping for a more adequate database structure
const user: UserType = {
id: db_user.id,
username: db_user.username,
password: db_user.password,
roles: db_user.roles.split(';') as Role[],
};
console.log(user);
return user;
}
async doSOmething() {
this.dataSource.query('SELECT * from users');
}
// async create(
// username: string,
// password: string,
// roles: Role[],
// ): Promise<User | undefined> {
// const roles_string = roles.join(';');
// const create_result = this.usersRepository.create(
// new User(username, password, roles_string),
// );
// }
}

24
back/test/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
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!');
});
});

9
back/test/jest-e2e.json Normal file
View File

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

4
back/tsconfig.build.json Normal file
View File

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

21
back/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: "1.1"
services:
mysql:
image: mysql
restart: always
container_name: db-mysql
ports:
- 3307:3306
environment:
MYSQL_DATABASE: path
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: dbuser
MYSQL_PASSWORD: securepassword
volumes:
- ./mysql/dump:/docker-entrypoint-initdb.d
networks:
- newnet
backend:
build:
context: ./back
dockerfile: Dockerfile
container_name: nestjs-app
ports:
- "3000:3000"
environment:
DB_TYPE: "mysql"
DB_HOST: "db-mysql"
DB_PORT: "3306"
DB_USERNAME: "dbuser"
DB_PASSWORD: "securepassword"
networks:
- newnet
networks:
newnet:
name: full_stack_network

View File

@@ -0,0 +1,9 @@
import styles from "./{{kebabCase name}}.module.scss";
type {{pascalCase name}}Props = {}
export const {{pascalCase name}}:React.FC<{{pascalCase name}}Props> = ({}) => {
return (
<div data-testid="{{{kebabCase name}}}" className={styles.container}></div>
)
}

View File

@@ -0,0 +1,2 @@
.container {
}

View File

@@ -0,0 +1,3 @@
"use client"
export { {{pascalCase name}} as default } from "./{{kebabCase name}}.component";

3
front/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
front/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
front/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

14
front/next.config.mjs Normal file
View File

@@ -0,0 +1,14 @@
import path from "path";
/** @type {import('next').NextConfig} */
const nextConfig = {
/**
* https://nextjs.org/docs/app/building-your-application/styling/sass
*/
sassOptions: {
prependData: `@import "@/styles/variables";`,
includePaths: [path.join("@", "styles")],
},
};
export default nextConfig;

4377
front/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

25
front/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "front-next-archetype",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"sass": "^1.77.4"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"typescript": "^5"
}
}

BIN
front/src/app/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

22
front/src/app/layout.tsx Normal file
View File

@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@/styles/main.scss";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Personal finance - Home",
description: "Manage your money blablabla",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

10
front/src/app/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import ThemeSwitcher from "@/modules/common/theme-switcher";
export default function Home() {
return (
<main>
Main page
<ThemeSwitcher />
</main>
);
}

View File

@@ -0,0 +1,3 @@
"use client";
export { ThemeSwitcher as default } from "./theme-switcher.component";

View File

@@ -0,0 +1,20 @@
import React from "react";
// TODO: make it consistent and link current data-theme with checked state fo the checkbox, maybe with a react state
export const ThemeSwitcher = () => {
const changeHandler = () => {
const isDarkTheme =
document.documentElement.getAttribute("data-theme") === "dark";
document.documentElement.setAttribute(
"data-theme",
isDarkTheme ? "light" : "dark",
);
};
return (
<label className="switch">
<input type="checkbox" onClick={changeHandler} />
<span className="slider round"></span>
</label>
);
};

View File

@@ -0,0 +1,8 @@
$default-font-size: 1.6rem;
$color-primary: #ff7730;
$color-secondary: #ff7730;
$color-background: var(--color-white);
$color-foreground: var(--color-black);
$color-white: var(--color-white);

View File

@@ -0,0 +1,51 @@
// For variable definition
:root {
--color-white: #fafafa;
--color-black: #101010;
}
[data-theme="dark"] {
--color-white: #101010;
--color-black: #fafafa;
}
*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
max-width: 100vw;
overflow-x: hidden;
font-size: 62.5%; // 1rem = 10px; 10px/16px = 62.5%
@media only screen and (min-width: 112.5em) {
font-size: 75%;
}
@media only screen and (max-width: 75em) {
font-size: 56.25%;
}
@media only screen and (max-width: 56.25em) {
font-size: 50%;
}
}
body {
box-sizing: border-box;
background-color: $color-background;
color: $color-foreground;
}
a {
color: inherit;
text-decoration: none;
}
::selection {
background-color: $color-primary;
color: $color-white;
}

26
front/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

16
mysql/docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
version: "0.1"
services:
mysql:
image: mysql
restart: always
container_name: db-mysql
ports:
- 3307:3306
environment:
MYSQL_DATABASE: path
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: dbuser
MYSQL_PASSWORD: securepassword
volumes:
- ./dump:/docker-entrypoint-initdb.d

12
mysql/dump/dump.sql Executable file
View File

@@ -0,0 +1,12 @@
-- Arch tables
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
roles VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO user (username, password, roles) VALUES
('dqnid', '$2b$10$DDGh9u4yiTY9FJx2faFPlucBbTB6Y3i8YkH7AEztcpeaKv08AjdrW', 'admin'),
('albi', '$2b$10$DDGh9u4yiTY9FJx2faFPlucBbTB6Y3i8YkH7AEztcpeaKv08AjdrW', 'admin');