Merge pull request #1 from dqnid/feature/mono-branch
Feature/mono branch
This commit is contained in:
122 files changed
+9503
-5557
No files matched your search
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert"
|
||||
]
|
||||
],
|
||||
"scope-case": [2, "always", "lower-case"],
|
||||
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
|
||||
"subject-empty": [2, "never"],
|
||||
"subject-full-stop": [2, "never", "."],
|
||||
"header-max-length": [2, "always", 72]
|
||||
}
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
echo -e "\e[35m➤ [COMMIT]\e[0m Checking message\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
npx commitlint --edit $1
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "\e[31m➤ [COMMIT]\e[0m Wrong format on message\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
exit -1
|
||||
fi
|
||||
echo -e "\e[32m➤ [COMMIT]\e[0m Message ok\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd ./front
|
||||
echo -e "\e[35m➤ [PRETTIER]\e[0m Formatting front files\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
npx prettier . --write --log-level silent
|
||||
|
||||
echo -e "\e[35m➤ [ESLINT]\e[0m Checking front code\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
npx lint-staged --silent
|
||||
echo $?
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "\e[31m➤ [ESLINT]\e[0m Errors in code\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
exit -1;
|
||||
fi
|
||||
echo -e "\e[32m➤ [ESLINT]\e[0m Code ok\e[37m - \e[33m$(date +"%H:%M:%S")\e[0m"
|
||||
|
||||
cd ..
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Router } from "express";
|
||||
import { ResponseSuccess } from "@/utils/response/response-success.model";
|
||||
|
||||
import {{pascalCase name}}Service from "./{{kebabCase name}}.service";
|
||||
|
||||
export const {{camelCase name}}Routes = Router();
|
||||
|
||||
const {{camelCase name}}Service = new {{pascalCase name}}Service();
|
||||
|
||||
{{camelCase name}}Routes.get("/", async (_, res) => {
|
||||
try {
|
||||
const response = await {{camelCase name}}Service.getAll{{pascalCase name}}();
|
||||
res.status(200);
|
||||
res.send(new ResponseSuccess(response));
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
export { {{camelCase name}}Service };
|
||||
@@ -0,0 +1,9 @@
|
||||
class {{pascalCase name}}Service {
|
||||
constructor(private info?: string) {}
|
||||
|
||||
async getAll{{pascalCase name}}() {
|
||||
return Promise.resolve(this.info);
|
||||
}
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}Service;
|
||||
@@ -0,0 +1,144 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:21-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "serve"]
|
||||
Generated
+2388
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "express-backend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/app.ts",
|
||||
"start": "ts-node src/app.ts",
|
||||
"build": "tsc",
|
||||
"serve": "node dist/app.js"
|
||||
},
|
||||
"author": "Daniel Heras Quesada",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.7.4",
|
||||
"express": "^4.21.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.11.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import express from "express";
|
||||
import config from "./config";
|
||||
import cors from "cors";
|
||||
import { routes } from "./modules";
|
||||
import { authorizationMiddleware } from "./modules/auth/auth.middleware";
|
||||
|
||||
const app = express();
|
||||
const port = config.port;
|
||||
|
||||
if (config.enableCors) {
|
||||
app.use(cors());
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Global middleware
|
||||
app.use(authorizationMiddleware as any); // TODO: move out of here
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`LOG: new ${req.method} request for ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Route specific middleware
|
||||
app.use("/example", (req, _res, next) => {
|
||||
console.log(`LOG: new ${req.method} example request for ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(config.baseRoute, routes); // / serves as the base for the imported routes
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running at http://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { configDotenv } from "dotenv";
|
||||
|
||||
configDotenv();
|
||||
|
||||
const config = {
|
||||
enableCors: true,
|
||||
port: process.env.PORT || 3000,
|
||||
baseRoute: "/api/v1",
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,26 @@
|
||||
import mysql, { QueryError } from "mysql2/promise";
|
||||
import { ResponseError } from "./utils/response/response-error.model";
|
||||
|
||||
const db_pool = mysql.createPool({
|
||||
host: process.env.DB_HOST ?? "localhost",
|
||||
port: parseInt(process.env.DB_PORT ?? "3307"),
|
||||
user: process.env.DB_USERNAME ?? "dbuser",
|
||||
password: process.env.DB_PASSWORD ?? "securepassword",
|
||||
database: process.env.DB_MAIN ?? "path",
|
||||
});
|
||||
|
||||
async function DB_Query<T>(query: string): Promise<Partial<T>[]> {
|
||||
try {
|
||||
const [results, _fields] = await db_pool.query(query);
|
||||
return results as T[];
|
||||
} catch (e) {
|
||||
const queryError = e as QueryError;
|
||||
throw new ResponseError({
|
||||
code: queryError.code,
|
||||
number: queryError.errno,
|
||||
detail: queryError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { db_pool as db_connection, DB_Query as db_query };
|
||||
@@ -0,0 +1 @@
|
||||
export const AUTH_KEY_SIZE = 30;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { authService } from "./auth.routes";
|
||||
import { Role } from "../users/users.types";
|
||||
import { ResponseError } from "../../utils/response/response-error.model";
|
||||
import config from "../../config";
|
||||
|
||||
async function authorization(req: Request, res: Response, next: NextFunction) {
|
||||
const resource = req.url.replace(config.baseRoute, "");
|
||||
const isPublic = authorizationTable.some((permission) => {
|
||||
const regex = new RegExp(permission.resource);
|
||||
return (
|
||||
regex.test(resource) &&
|
||||
permission.actions.includes(req.method as HttpMethod) &&
|
||||
permission.roles.includes(Role.Public)
|
||||
);
|
||||
});
|
||||
|
||||
if (isPublic) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = req.get("authorization");
|
||||
const token = authHeader?.split(" ")[1];
|
||||
|
||||
if (token == null) {
|
||||
res.status(401);
|
||||
return res.send(new ResponseError(401));
|
||||
}
|
||||
|
||||
const user = authService.verifyToken(token);
|
||||
if (user && user instanceof Object) {
|
||||
const authorized = authorizationTable.some((permission) => {
|
||||
const regex = new RegExp(permission.resource);
|
||||
return (
|
||||
regex.test(resource) &&
|
||||
permission.roles.some((role) => user.roles.includes(role)) &&
|
||||
permission.actions.includes(req.method as HttpMethod)
|
||||
);
|
||||
});
|
||||
if (authorized) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
res.status(401);
|
||||
return res.send(new ResponseError(401));
|
||||
}
|
||||
|
||||
type HttpMethod =
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "PATCH"
|
||||
| "OPTIONS"
|
||||
| "HEAD";
|
||||
type Permissions = {
|
||||
roles: Array<Role>;
|
||||
resource: string;
|
||||
actions: Array<HttpMethod>;
|
||||
};
|
||||
|
||||
const authorizationTable: Permissions[] = [
|
||||
{
|
||||
roles: [Role.Public],
|
||||
resource: "^/auth.*", // begins with /auth (is auth would be ^\/auth$)
|
||||
actions: ["POST"],
|
||||
},
|
||||
{
|
||||
roles: [Role.Admin],
|
||||
resource: "^/users.*",
|
||||
actions: ["GET", "PUT", "POST", "DELETE"],
|
||||
},
|
||||
{
|
||||
roles: [Role.User],
|
||||
resource: "^/users.*",
|
||||
actions: ["GET"],
|
||||
},
|
||||
{
|
||||
roles: [Role.Public],
|
||||
resource: "^/example.*",
|
||||
actions: ["GET", "OPTIONS"],
|
||||
},
|
||||
];
|
||||
|
||||
export { authorization as authorizationMiddleware };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Router } from "express";
|
||||
import { userService } from "../users/users.routes";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ResponseError } from "../../utils/response/response-error.model";
|
||||
|
||||
export const authRoutes = Router();
|
||||
|
||||
const authService = new AuthService(userService);
|
||||
|
||||
authRoutes.post("/login", async (req, res) => {
|
||||
try {
|
||||
if (!req.body) {
|
||||
res.status(400);
|
||||
res.send(new ResponseError(400));
|
||||
}
|
||||
const token = await authService.signIn(
|
||||
req.body.username,
|
||||
req.body.password,
|
||||
);
|
||||
if (token) {
|
||||
res.status(200);
|
||||
res.send({ access_token: token });
|
||||
} else {
|
||||
res.status(401);
|
||||
res.send(new ResponseError(401));
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
export { authService };
|
||||
@@ -0,0 +1,51 @@
|
||||
import { sign, verify } from "jsonwebtoken";
|
||||
import UsersService from "../users/users.service";
|
||||
import { compare } from "bcrypt";
|
||||
import { generateRandomString } from "./auth.utils";
|
||||
import { AUTH_KEY_SIZE } from "./auth.constants";
|
||||
import { Role } from "../users/users.types";
|
||||
|
||||
export class AuthService {
|
||||
private secret_key: string;
|
||||
constructor(private usersService: UsersService) {
|
||||
this.secret_key = generateRandomString(AUTH_KEY_SIZE);
|
||||
}
|
||||
|
||||
async signIn(username: string, password: string) {
|
||||
const user = await this.usersService.getUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSamePasswd = await compare(`${password}`, `${user?.password}`);
|
||||
|
||||
if (!isSamePasswd) return null;
|
||||
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
username: user.username,
|
||||
roles: user.roles,
|
||||
picture: user.picture,
|
||||
};
|
||||
|
||||
const token = sign(payload, this.secret_key, { expiresIn: 60 * 60 });
|
||||
return token;
|
||||
}
|
||||
|
||||
verifyToken(jwt: string) {
|
||||
const token = jwt.split(".")[1];
|
||||
if (!token) return false;
|
||||
try {
|
||||
const payload = verify(jwt, this.secret_key);
|
||||
if (payload instanceof Object)
|
||||
return {
|
||||
username: payload.username as string,
|
||||
roles: payload.roles as Role[],
|
||||
};
|
||||
else return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
||||
function generateRandomString(size: number) {
|
||||
const value = Math.random() * Math.pow(10, size);
|
||||
return btoa(value.toString());
|
||||
}
|
||||
|
||||
export { generateRandomString };
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Router } from "express";
|
||||
|
||||
export const defaultRoute = Router();
|
||||
|
||||
defaultRoute.get("/", (_, res) => {
|
||||
res.send("Nothing to see here");
|
||||
res.status(200);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import ExampleService from "./example.service";
|
||||
import { ResponseSuccess } from "../../utils/response/response-success.model";
|
||||
import { ResponseError } from "../../utils/response/response-error.model";
|
||||
|
||||
export const exampleRoutes = Router();
|
||||
|
||||
const exampleService = new ExampleService();
|
||||
|
||||
exampleRoutes.get("/", async (_, res) => {
|
||||
try {
|
||||
const response = await exampleService.getAllExamples();
|
||||
res.status(200);
|
||||
res.send(new ResponseSuccess(response));
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
exampleRoutes.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const response = await exampleService.getExampleById(
|
||||
parseInt(req.params.id),
|
||||
);
|
||||
if (response) {
|
||||
res.status(200);
|
||||
res.send(new ResponseSuccess(response));
|
||||
} else {
|
||||
res.status(404);
|
||||
res.send(new ResponseError({ code: "NOT_FOUND", number: 404 }));
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
export { exampleService };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { db_query } from "../../db";
|
||||
import { Example } from "./example.types";
|
||||
|
||||
class ExampleService {
|
||||
constructor(private info?: string) {}
|
||||
|
||||
async getAllExamples(): Promise<Example[]> {
|
||||
const examples = await db_query("select * from example");
|
||||
return examples as Example[];
|
||||
}
|
||||
|
||||
async getExampleById(id: number): Promise<Example | null> {
|
||||
const example = await db_query(
|
||||
`select * from example as example WHERE id = ${id};`,
|
||||
);
|
||||
if (example.length) {
|
||||
return example[0] as Example;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default ExampleService;
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Example = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
image: string;
|
||||
created_at: string;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import express from "express";
|
||||
|
||||
import { defaultRoute } from "./default.route";
|
||||
import { userRoutes } from "./users/users.routes";
|
||||
import { authRoutes } from "./auth/auth.routes";
|
||||
import { exampleRoutes } from "./example/example.routes";
|
||||
|
||||
export const routes = express.Router();
|
||||
|
||||
/*
|
||||
* Routes
|
||||
* */
|
||||
routes.use("/", defaultRoute);
|
||||
routes.use("/users", userRoutes);
|
||||
routes.use("/auth", authRoutes);
|
||||
routes.use("/example", exampleRoutes);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Router } from "express";
|
||||
import UsersService from "./users.service";
|
||||
import { sanitize_user } from "./users.utils";
|
||||
import { ResponseSuccess } from "../../utils/response/response-success.model";
|
||||
import { ResponseError } from "../../utils/response/response-error.model";
|
||||
|
||||
export const userRoutes = Router();
|
||||
|
||||
const userService = new UsersService();
|
||||
|
||||
//TODO: block access to NON-admins or simply comment
|
||||
userRoutes.get("/", async (_, res) => {
|
||||
try {
|
||||
const response = await userService.getAllUsers();
|
||||
res.status(200);
|
||||
res.send(new ResponseSuccess(response.map((user) => sanitize_user(user))));
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
userRoutes.get("/:username", async (req, res) => {
|
||||
try {
|
||||
const response = await userService.getUserByUsername(req.params.username);
|
||||
if (response) {
|
||||
res.status(200);
|
||||
res.send(new ResponseSuccess(sanitize_user(response)));
|
||||
} else {
|
||||
res.status(404);
|
||||
res.send(new ResponseError({ code: "NOT_FOUND", number: 404 }));
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500);
|
||||
res.send(e);
|
||||
}
|
||||
});
|
||||
|
||||
export { userService };
|
||||
@@ -0,0 +1,31 @@
|
||||
import { db_query } from "../../db";
|
||||
import { User } from "./users.types";
|
||||
|
||||
class UsersService {
|
||||
constructor() {}
|
||||
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
const data = await db_query("select * from user");
|
||||
const users: User[] = data.map((user: any) => ({
|
||||
...user,
|
||||
roles: user.roles.split(";"),
|
||||
}));
|
||||
return users;
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | null> {
|
||||
const data = await db_query(
|
||||
`select * from user as user WHERE LOWER(username) = LOWER('${username}');`,
|
||||
);
|
||||
if (data.length) {
|
||||
const user = {
|
||||
...(data[0] as User),
|
||||
roles: (data[0] as any).roles.split(";"),
|
||||
};
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default UsersService;
|
||||
@@ -0,0 +1,17 @@
|
||||
export enum Role {
|
||||
Public = "public",
|
||||
User = "user",
|
||||
Manager = "manager",
|
||||
Admin = "admin",
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
username: string;
|
||||
password: string;
|
||||
roles: Role[];
|
||||
picture: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type UserDetailResult = Omit<User, "password">;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { User, UserDetailResult } from "./users.types";
|
||||
|
||||
// TODO: prettify this
|
||||
export function sanitize_user(user: User): UserDetailResult {
|
||||
delete (user as any).password;
|
||||
return user;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
type BasicError = {
|
||||
code: string | number;
|
||||
number?: number;
|
||||
detail?: string;
|
||||
status?: number;
|
||||
suggestion?: string;
|
||||
};
|
||||
|
||||
const code_error_mapping: Record<number, BasicError> = {
|
||||
400: {
|
||||
code: "BAD REQUEST",
|
||||
status: 400,
|
||||
},
|
||||
401: {
|
||||
code: "UNAUTHORIZED",
|
||||
status: 401,
|
||||
},
|
||||
};
|
||||
|
||||
export class ResponseError extends Error {
|
||||
public error: BasicError | number;
|
||||
constructor(
|
||||
error: BasicError | number,
|
||||
public timestamp: number = Date.now(),
|
||||
) {
|
||||
super();
|
||||
if (error instanceof Object) {
|
||||
this.error = error;
|
||||
} else {
|
||||
this.error = code_error_mapping[error];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export class ResponseSuccess {
|
||||
public data?: unknown;
|
||||
public results?: unknown[];
|
||||
public count?: number;
|
||||
|
||||
constructor(data: unknown) {
|
||||
if (data instanceof Array) {
|
||||
this.results = data;
|
||||
this.count = data.length;
|
||||
} else if (data instanceof Object) {
|
||||
Object.assign(this, data);
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9
-1
@@ -1,6 +1,14 @@
|
||||
version: "1.2"
|
||||
|
||||
services:
|
||||
# front:
|
||||
# build:
|
||||
# context: ./front
|
||||
# dockerfile: Dockerfile
|
||||
# container_name: nextjs-app
|
||||
# ports:
|
||||
# - "3016:3016"
|
||||
#
|
||||
mysql:
|
||||
image: mysql
|
||||
restart: always
|
||||
@@ -19,7 +27,7 @@ services:
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./back
|
||||
context: ./back-express
|
||||
dockerfile: Dockerfile
|
||||
container_name: nestjs-app
|
||||
ports:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import styles from "./.module.scss";
|
||||
|
||||
type Props = {}
|
||||
|
||||
export const :React.FC<Props> = ({}) => {
|
||||
return (
|
||||
<div data-testid="{}" className={styles.container}></div>
|
||||
)
|
||||
}
|
||||
@@ -4,3 +4,7 @@ NEXT_PUBLIC_API_URL=http://localhost:3016
|
||||
# GEN AUTH_SECRET: $ openssl rand -base64 32
|
||||
NEXTAUTH_SECRET=6lHRWUvCBtqlgTWc6aFn6s6PudYjuN6oUY+RrcEntTU=
|
||||
NEXTAUTH_URL=http://localhost:3016
|
||||
|
||||
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/api/v1
|
||||
NEXT_PUBLIC_BACKEND_URL=http://localhost:3000/api/v1
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
*.[tj]s
|
||||
*.[tj]sx
|
||||
|
||||
!src/**/*
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["next/core-web-vitals", "prettier"]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
.container {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Ignore artifacts:
|
||||
.next
|
||||
.blueprints
|
||||
node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 85,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:21-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3016
|
||||
|
||||
# Start the NestJS application
|
||||
CMD ["npm", "run", "start"]
|
||||
+2
-2
@@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
|
||||
|
||||
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.
|
||||
- [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!
|
||||
|
||||
|
||||
+17
-17
@@ -2,23 +2,23 @@ import path from "path";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
/**
|
||||
* https://nextjs.org/docs/app/building-your-application/styling/sass
|
||||
*/
|
||||
sassOptions: {
|
||||
// optional: prependData: `@import "@/styles/variables";`,
|
||||
includePaths: [path.join("@", "styles")],
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "picsum.photos",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
/**
|
||||
* https://nextjs.org/docs/app/building-your-application/styling/sass
|
||||
*/
|
||||
sassOptions: {
|
||||
// optional: prependData: `@import "@/styles/variables";`,
|
||||
includePaths: [path.join("@", "styles")],
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "picsum.photos",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+5319
-4582
File diff suppressed because it is too large.
Load diff
+28
-26
@@ -1,28 +1,30 @@
|
||||
{
|
||||
"name": "front-next-archetype",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3016",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3016",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"postcss": "^8.4.39",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"prettier": "3.3.3",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"name": "front-next-archetype",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3016",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3016",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"postcss": "^8.4.39",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "3.3.3",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
Main page
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
Main page
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+29
-29
@@ -2,38 +2,38 @@ import { withAuth } from "next-auth/middleware";
|
||||
import { authOptions } from "./modules/auth/configs/auth.options";
|
||||
|
||||
export default withAuth({
|
||||
pages: authOptions.pages,
|
||||
callbacks: {
|
||||
authorized({ req, token }) {
|
||||
if (token && token.apiSession.exp * 1000 > Date.now()) {
|
||||
return true;
|
||||
}
|
||||
const pathname = req.nextUrl.pathname;
|
||||
return (
|
||||
pathname.startsWith("/_next/") ||
|
||||
pathname.startsWith("/favicon.ico") ||
|
||||
pathname.startsWith("/assets/")
|
||||
);
|
||||
pages: authOptions.pages,
|
||||
callbacks: {
|
||||
authorized({ req, token }) {
|
||||
if (token && token.apiSession.exp * 1000 > Date.now()) {
|
||||
return true;
|
||||
}
|
||||
const pathname = req.nextUrl.pathname;
|
||||
return (
|
||||
pathname.startsWith("/_next/") ||
|
||||
pathname.startsWith("/favicon.ico") ||
|
||||
pathname.startsWith("/assets/")
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const value = {
|
||||
token: {
|
||||
name: "dqnid",
|
||||
picture: "https://picsum.photos/200/300",
|
||||
sub: "dqnid",
|
||||
user: {
|
||||
id: "dqnid",
|
||||
roles: ["user", "manager", "admin"],
|
||||
image: "https://picsum.photos/200/300",
|
||||
name: "dqnid",
|
||||
token: {
|
||||
name: "dqnid",
|
||||
picture: "https://picsum.photos/200/300",
|
||||
sub: "dqnid",
|
||||
user: {
|
||||
id: "dqnid",
|
||||
roles: ["user", "manager", "admin"],
|
||||
image: "https://picsum.photos/200/300",
|
||||
name: "dqnid",
|
||||
},
|
||||
apiSession: {
|
||||
exp: 1725398177,
|
||||
},
|
||||
iat: 1725394577,
|
||||
exp: 1727986577,
|
||||
jti: "3203d3c7-dc27-4599-b37e-16737b3a6674",
|
||||
},
|
||||
apiSession: {
|
||||
exp: 1725398177,
|
||||
},
|
||||
iat: 1725394577,
|
||||
exp: 1727986577,
|
||||
jti: "3203d3c7-dc27-4599-b37e-16737b3a6674",
|
||||
},
|
||||
};
|
||||
@@ -1,179 +1,179 @@
|
||||
.container {
|
||||
max-width: 50em;
|
||||
height: fit-content;
|
||||
max-width: 50em;
|
||||
height: fit-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
font-size: 1.4rem;
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 0.4rem;
|
||||
border: 2px solid rgba(var(--color-grey-90-rgb), 0.4);
|
||||
background-color: var(--color-white);
|
||||
|
||||
.side__illustration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-width: 16em;
|
||||
box-sizing: content-box;
|
||||
opacity: 0.8;
|
||||
flex-direction: row;
|
||||
|
||||
text-shadow: rgba(var(--color-grey-10), 0.3);
|
||||
padding: 2rem 2rem;
|
||||
font-size: 1.4rem;
|
||||
overflow: hidden;
|
||||
|
||||
--s: 100px;
|
||||
--c1: rgba(var(--color-grey-70-rgb), 0.4);
|
||||
--c2: var(--color-white);
|
||||
border-radius: 0.4rem;
|
||||
border: 2px solid rgba(var(--color-grey-90-rgb), 0.4);
|
||||
background-color: var(--color-white);
|
||||
|
||||
--_g: #0000 90deg, var(--c1) 0;
|
||||
background: conic-gradient(from 90deg at 2px 2px, var(--_g)),
|
||||
conic-gradient(from 90deg at 1px 1px, var(--_g)), var(--c2);
|
||||
background-size:
|
||||
var(--s) var(--s),
|
||||
calc(var(--s) / 5) calc(var(--s) / 5);
|
||||
background-position: -2px -2px;
|
||||
.side__illustration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-width: 16em;
|
||||
box-sizing: content-box;
|
||||
opacity: 0.8;
|
||||
|
||||
& > h1 {
|
||||
font-size: 3.4rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
text-shadow: rgba(var(--color-grey-10), 0.3);
|
||||
padding: 2rem 2rem;
|
||||
|
||||
padding-right: 2rem;
|
||||
--s: 100px;
|
||||
--c1: rgba(var(--color-grey-70-rgb), 0.4);
|
||||
--c2: var(--color-white);
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
--_g: #0000 90deg, var(--c1) 0;
|
||||
background: conic-gradient(from 90deg at 2px 2px, var(--_g)),
|
||||
conic-gradient(from 90deg at 1px 1px, var(--_g)), var(--c2);
|
||||
background-size:
|
||||
var(--s) var(--s),
|
||||
calc(var(--s) / 5) calc(var(--s) / 5);
|
||||
background-position: -2px -2px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 40%;
|
||||
width: 110%;
|
||||
top: 70%;
|
||||
left: -5%;
|
||||
& > h1 {
|
||||
font-size: 3.4rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
|
||||
z-index: -1;
|
||||
padding-right: 2rem;
|
||||
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
& > span {
|
||||
position: relative;
|
||||
|
||||
& > h4 {
|
||||
font-size: 1.4rem;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 40%;
|
||||
width: 110%;
|
||||
top: 70%;
|
||||
left: -5%;
|
||||
|
||||
text-align: right;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
z-index: -1;
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
|
||||
padding: 3.5em 2.5em;
|
||||
|
||||
background: rgba(var(--color-grey-90-rgb), 0.4);
|
||||
|
||||
&__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: 0.8em;
|
||||
|
||||
color: rgba(var(--color-grey-60-rgb), 0.8);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&__input {
|
||||
padding: 0.8em;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--color-grey-70);
|
||||
border-radius: 2px;
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: rgba(var(--color-grey-60-rgb), 0.8);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
box-shadow: 0 1px 4px rgba(var(--color-primary-dark-rgb), 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__group:has(.form__input:placeholder-shown) .form__label {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
|
||||
& .submit__button {
|
||||
all: unset;
|
||||
width: fit-content;
|
||||
font-size: 1.4rem;
|
||||
text-transform: uppercase;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 2px;
|
||||
|
||||
align-self: center;
|
||||
|
||||
border: none;
|
||||
color: var(--color-black);
|
||||
border: 2px solid var(--color-primary);
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
background-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 6px rgba(var(--color-primary-dark-rgb), 0.4);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
& > h4 {
|
||||
font-size: 1.4rem;
|
||||
|
||||
text-align: right;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
opacity: 0;
|
||||
height: 2.5rem;
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
|
||||
align-self: flex-start;
|
||||
padding: 3.5em 2.5em;
|
||||
|
||||
transition: opacity 0.3s ease-in;
|
||||
background: rgba(var(--color-grey-90-rgb), 0.4);
|
||||
|
||||
&--wrong {
|
||||
color: var(--color-error);
|
||||
}
|
||||
&__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-left: 0.8em;
|
||||
|
||||
color: rgba(var(--color-grey-60-rgb), 0.8);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&__input {
|
||||
padding: 0.8em;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--color-grey-70);
|
||||
border-radius: 2px;
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: rgba(var(--color-grey-60-rgb), 0.8);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
box-shadow: 0 1px 4px rgba(var(--color-primary-dark-rgb), 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__group:has(.form__input:placeholder-shown) .form__label {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
|
||||
& .submit__button {
|
||||
all: unset;
|
||||
width: fit-content;
|
||||
font-size: 1.4rem;
|
||||
text-transform: uppercase;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 2px;
|
||||
|
||||
align-self: center;
|
||||
|
||||
border: none;
|
||||
color: var(--color-black);
|
||||
border: 2px solid var(--color-primary);
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
background-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 6px rgba(var(--color-primary-dark-rgb), 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
opacity: 0;
|
||||
height: 2.5rem;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
transition: opacity 0.3s ease-in;
|
||||
|
||||
&--wrong {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.form--error {
|
||||
.form__input {
|
||||
border-bottom: 2px solid var(--color-error);
|
||||
}
|
||||
.form__input {
|
||||
border-bottom: 2px solid var(--color-error);
|
||||
}
|
||||
|
||||
.form__message {
|
||||
opacity: 1;
|
||||
}
|
||||
.form__message {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -7,101 +7,101 @@ import { signIn, useSession } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
type LogInWidgetsProps = {
|
||||
afterSuccess?: Function;
|
||||
afterSuccess?: Function;
|
||||
};
|
||||
|
||||
export const LogInWidget: React.FC<LogInWidgetsProps> = ({ afterSuccess }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const callbackUrl = searchParams.get("callbackUrl");
|
||||
const callbackUrl = searchParams.get("callbackUrl");
|
||||
|
||||
const [loginStatus, setLoginStatus] = useState<
|
||||
"idle" | "check" | "confirm" | "error"
|
||||
>("idle");
|
||||
const [loginStatus, setLoginStatus] = useState<
|
||||
"idle" | "check" | "confirm" | "error"
|
||||
>("idle");
|
||||
|
||||
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLoginStatus("check");
|
||||
const username = (
|
||||
event.currentTarget.elements.namedItem("username") as HTMLInputElement
|
||||
).value;
|
||||
const password = (
|
||||
event.currentTarget.elements.namedItem("password") as HTMLInputElement
|
||||
).value;
|
||||
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setLoginStatus("check");
|
||||
const username = (
|
||||
event.currentTarget.elements.namedItem("username") as HTMLInputElement
|
||||
).value;
|
||||
const password = (
|
||||
event.currentTarget.elements.namedItem("password") as HTMLInputElement
|
||||
).value;
|
||||
|
||||
const result = await signIn("credentials", {
|
||||
redirect: false,
|
||||
username: username,
|
||||
password: password,
|
||||
});
|
||||
const result = await signIn("credentials", {
|
||||
redirect: false,
|
||||
username: username,
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (!result?.ok) {
|
||||
setLoginStatus("error");
|
||||
} else {
|
||||
setLoginStatus("confirm");
|
||||
}
|
||||
};
|
||||
if (!result?.ok) {
|
||||
setLoginStatus("error");
|
||||
} else {
|
||||
setLoginStatus("confirm");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === "confirm") {
|
||||
router.push(callbackUrl ?? "/");
|
||||
}
|
||||
}, [loginStatus]);
|
||||
useEffect(() => {
|
||||
if (loginStatus === "confirm") {
|
||||
router.push(callbackUrl ?? "/");
|
||||
}
|
||||
}, [loginStatus]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles["side__illustration"]}>
|
||||
<h1>
|
||||
Welcome to this <span>page!</span>
|
||||
</h1>
|
||||
<h4>Use your credentials to log-in</h4>
|
||||
</div>
|
||||
<form
|
||||
className={`${styles.form} ${loginStatus === "error" && styles["form--error"]} ${loginStatus === "check" && styles["form--loading"]}`}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<div className={`${styles["form__group"]}`}>
|
||||
<label htmlFor="username" className={styles["form__label"]}>
|
||||
username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="username"
|
||||
className={styles["form__input"]}
|
||||
required
|
||||
/>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles["side__illustration"]}>
|
||||
<h1>
|
||||
Welcome to this <span>page!</span>
|
||||
</h1>
|
||||
<h4>Use your credentials to log-in</h4>
|
||||
</div>
|
||||
<form
|
||||
className={`${styles.form} ${loginStatus === "error" && styles["form--error"]} ${loginStatus === "check" && styles["form--loading"]}`}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<div className={`${styles["form__group"]}`}>
|
||||
<label htmlFor="username" className={styles["form__label"]}>
|
||||
username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="username"
|
||||
className={styles["form__input"]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["form__group"]}>
|
||||
<label htmlFor="password" className={styles["form__label"]}>
|
||||
password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="password"
|
||||
className={styles["form__input"]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["form__actions"]}>
|
||||
<div className={styles["form__message"]}>
|
||||
{loginStatus === "error" ? (
|
||||
<p className={styles["form__message--wrong"]}>
|
||||
Wrong credentials, try again
|
||||
</p>
|
||||
) : (
|
||||
<>Loading...</>
|
||||
)}
|
||||
</div>
|
||||
<button type="submit" className={styles["submit__button"]}>
|
||||
Log-in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className={styles["form__group"]}>
|
||||
<label htmlFor="password" className={styles["form__label"]}>
|
||||
password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="password"
|
||||
className={styles["form__input"]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["form__actions"]}>
|
||||
<div className={styles["form__message"]}>
|
||||
{loginStatus === "error" ? (
|
||||
<p className={styles["form__message--wrong"]}>
|
||||
Wrong credentials, try again
|
||||
</p>
|
||||
) : (
|
||||
<>Loading...</>
|
||||
)}
|
||||
</div>
|
||||
<button type="submit" className={styles["submit__button"]}>
|
||||
Log-in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
@@ -2,9 +2,9 @@ import { signIn } from "next-auth/react";
|
||||
import styles from "./sign-in-button.module.scss";
|
||||
|
||||
export const SignInButton = () => {
|
||||
return (
|
||||
<button className={styles.button} onClick={() => void signIn()}>
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button className={styles.button} onClick={() => void signIn()}>
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -2,12 +2,12 @@ import { signOut } from "next-auth/react";
|
||||
import styles from "./sign-out-button.module.scss";
|
||||
|
||||
export const SignOutButton = () => {
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() => void signOut({ callbackUrl: "/" })}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() => void signOut({ callbackUrl: "/" })}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -3,14 +3,14 @@ import UserPanel from "../user-panel";
|
||||
import { UserFrame } from "../user-frame";
|
||||
|
||||
export const UserDropdown = async () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles["avatar__container"]}>
|
||||
<UserFrame />
|
||||
</div>
|
||||
<div className={styles["dropdown__container"]}>
|
||||
<UserPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles["avatar__container"]}>
|
||||
<UserFrame />
|
||||
</div>
|
||||
<div className={styles["dropdown__container"]}>
|
||||
<UserPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,26 @@
|
||||
.container {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
.avatar__container {
|
||||
height: var(--header-item-height, 4rem);
|
||||
width: var(--header-item-height, 4rem);
|
||||
.avatar__container {
|
||||
height: var(--header-item-height, 4rem);
|
||||
width: var(--header-item-height, 4rem);
|
||||
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
|
||||
.dropdown__container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
.dropdown__container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
transition: all 0.2s;
|
||||
}
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
&:hover > .dropdown__container {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
&:hover > .dropdown__container {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@@ -5,39 +5,39 @@ import { getServerSession } from "next-auth";
|
||||
import SignInButton from "../sign-in-button";
|
||||
|
||||
export async function UserFrame() {
|
||||
const session = await getServerSession();
|
||||
const session = await getServerSession();
|
||||
|
||||
if (!session) {
|
||||
return <SignInButton />;
|
||||
}
|
||||
if (!session) {
|
||||
return <SignInButton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.frame}>
|
||||
<span className={styles.initials}>
|
||||
{getInitialsFromName(session?.user?.name || "00")}
|
||||
</span>
|
||||
<Image
|
||||
className={styles["profile__picture"]}
|
||||
src={session?.user?.image || ""}
|
||||
alt="user profile picture"
|
||||
fill={true}
|
||||
sizes="(max-width: 768px) 10rem, 6rem"
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.frame}>
|
||||
<span className={styles.initials}>
|
||||
{getInitialsFromName(session?.user?.name || "00")}
|
||||
</span>
|
||||
<Image
|
||||
className={styles["profile__picture"]}
|
||||
src={session?.user?.image || ""}
|
||||
alt="user profile picture"
|
||||
fill={true}
|
||||
sizes="(max-width: 768px) 10rem, 6rem"
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getInitialsFromName = (name: string): string => {
|
||||
if (name.length < 1) return "00";
|
||||
if (name.length < 1) return "00";
|
||||
|
||||
// TODO: this can be done in one line probably
|
||||
const words = name.split(" ");
|
||||
let initials = "";
|
||||
words.forEach((word) => (initials += word[0]));
|
||||
if (initials.length < 2) {
|
||||
initials += name[1];
|
||||
}
|
||||
// TODO: this can be done in one line probably
|
||||
const words = name.split(" ");
|
||||
let initials = "";
|
||||
words.forEach((word) => (initials += word[0]));
|
||||
if (initials.length < 2) {
|
||||
initials += name[1];
|
||||
}
|
||||
|
||||
return initials.slice(0, 2);
|
||||
return initials.slice(0, 2);
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
.frame {
|
||||
position: relative;
|
||||
font-size: inherit;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
box-shadow: 1px 1px 3px var(--color-grey-30);
|
||||
box-sizing: border-box;
|
||||
|
||||
& > span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
position: relative;
|
||||
font-size: inherit;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-grey-30);
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
box-shadow: 1px 1px 3px var(--color-grey-30);
|
||||
box-sizing: border-box;
|
||||
|
||||
& > span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
font-size: inherit;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-grey-30);
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,20 @@ import { useSession } from "next-auth/react";
|
||||
import ThemeSwitcher from "@/modules/common/components/theme-switcher";
|
||||
|
||||
export function UserPanel() {
|
||||
const session = useSession();
|
||||
const session = useSession();
|
||||
|
||||
return (
|
||||
<ul className={styles.frame}>
|
||||
<li className={styles["user__name"]}>{session.data?.user?.name}</li>
|
||||
<ul className={styles["user__roles"]}>
|
||||
{session.data?.user?.roles.map((role) => (
|
||||
<li key={role} className={styles["role__chip"]}>
|
||||
{role}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ThemeSwitcher />
|
||||
<SignOutButton />
|
||||
</ul>
|
||||
);
|
||||
return (
|
||||
<ul className={styles.frame}>
|
||||
<li className={styles["user__name"]}>{session.data?.user?.name}</li>
|
||||
<ul className={styles["user__roles"]}>
|
||||
{session.data?.user?.roles.map((role) => (
|
||||
<li key={role} className={styles["role__chip"]}>
|
||||
{role}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ThemeSwitcher />
|
||||
<SignOutButton />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1,39 @@
|
||||
.frame {
|
||||
list-style: none;
|
||||
min-width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.8rem 0.6rem;
|
||||
list-style: none;
|
||||
min-width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.8rem 0.6rem;
|
||||
|
||||
padding: 1.2rem 1.8rem;
|
||||
padding: 1.2rem 1.8rem;
|
||||
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-white);
|
||||
box-shadow: 0 2px 3px rgba(var(--color-grey-10-rgb), 0.2);
|
||||
color: var(--color-background);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-white);
|
||||
box-shadow: 0 2px 3px rgba(var(--color-grey-10-rgb), 0.2);
|
||||
color: var(--color-background);
|
||||
|
||||
z-index: 10;
|
||||
z-index: 10;
|
||||
|
||||
& .user {
|
||||
font-size: 1.6rem;
|
||||
& .user {
|
||||
font-size: 1.6rem;
|
||||
|
||||
&__name {
|
||||
&__name {
|
||||
}
|
||||
|
||||
&__roles {
|
||||
list-style: none;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
|
||||
& .role__chip {
|
||||
padding: 0.4rem 0.6rem;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-grey-90);
|
||||
color: var(--color-grey-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__roles {
|
||||
list-style: none;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
|
||||
& .role__chip {
|
||||
padding: 0.4rem 0.6rem;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-grey-90);
|
||||
color: var(--color-grey-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,85 +2,92 @@ import { AuthOptions, Role, User } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/log-in",
|
||||
signOut: "/",
|
||||
},
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Sign in",
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text", placeholder: "yourself" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const response = await fetch("http://localhost:3000/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: credentials?.username,
|
||||
password: credentials?.password,
|
||||
}),
|
||||
});
|
||||
|
||||
type LoginResponse = {
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
if (response.status < 200 || response.status > 399) return null;
|
||||
|
||||
const response_body = (await response.json()) as LoginResponse;
|
||||
|
||||
type TokenPayload = {
|
||||
sub: string;
|
||||
username: string;
|
||||
roles: Role[];
|
||||
picture: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
const token_payload = JSON.parse(
|
||||
atob(response_body.access_token.split(".")[1]),
|
||||
) as TokenPayload;
|
||||
|
||||
const user: User = {
|
||||
id: token_payload.username,
|
||||
roles: token_payload.roles,
|
||||
image: token_payload.picture,
|
||||
name: token_payload.username,
|
||||
apiSession: {
|
||||
exp: token_payload.exp,
|
||||
accessToken: response_body.access_token,
|
||||
},
|
||||
};
|
||||
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
// Persist the OAuth access_token to the token right after signin
|
||||
if (user) {
|
||||
const { apiSession, ...cleanedUser } = user;
|
||||
token.user = cleanedUser;
|
||||
token.apiSession = apiSession;
|
||||
}
|
||||
return token;
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
async session({ session, token }) {
|
||||
// Send properties to the client, like an access_token from a provider.
|
||||
if (token?.user && session) {
|
||||
session.apiSession = token.apiSession;
|
||||
session.user = token.user;
|
||||
}
|
||||
return session;
|
||||
pages: {
|
||||
signIn: "/auth/log-in",
|
||||
signOut: "/",
|
||||
},
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Sign in",
|
||||
credentials: {
|
||||
username: {
|
||||
label: "Username",
|
||||
type: "text",
|
||||
placeholder: "yourself",
|
||||
},
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_AUTH_URL + "/auth/login",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: credentials?.username,
|
||||
password: credentials?.password,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
type LoginResponse = {
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
if (response.status < 200 || response.status > 399) return null;
|
||||
|
||||
const response_body = (await response.json()) as LoginResponse;
|
||||
|
||||
type TokenPayload = {
|
||||
sub: string;
|
||||
username: string;
|
||||
roles: Role[];
|
||||
picture: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
const token_payload = JSON.parse(
|
||||
atob(response_body.access_token.split(".")[1]),
|
||||
) as TokenPayload;
|
||||
|
||||
const user: User = {
|
||||
id: token_payload.username,
|
||||
roles: token_payload.roles,
|
||||
image: token_payload.picture,
|
||||
name: token_payload.username,
|
||||
apiSession: {
|
||||
exp: token_payload.exp,
|
||||
accessToken: response_body.access_token,
|
||||
},
|
||||
};
|
||||
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
// Persist the OAuth access_token to the token right after signin
|
||||
if (user) {
|
||||
const { apiSession, ...cleanedUser } = user;
|
||||
token.user = cleanedUser;
|
||||
token.apiSession = apiSession;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
// Send properties to the client, like an access_token from a provider.
|
||||
if (token?.user && session) {
|
||||
session.apiSession = token.apiSession;
|
||||
session.user = token.user;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,5 +4,5 @@ import { SessionProvider } from "next-auth/react";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
};
|
||||
+21
-21
@@ -2,29 +2,29 @@ import NextAuth, { DefaultSession } from "next-auth";
|
||||
import { JWT, DefaultJWT } from "next-auth/jwt";
|
||||
|
||||
declare module "next-auth" {
|
||||
type Role = "user" | "manager" | "admin";
|
||||
interface ApiSession {
|
||||
exp?: number;
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
interface User {
|
||||
id: string;
|
||||
roles: Role[];
|
||||
image?: string;
|
||||
name?: string;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
type Role = "user" | "manager" | "admin";
|
||||
interface ApiSession {
|
||||
exp?: number;
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
interface User {
|
||||
id: string;
|
||||
roles: Role[];
|
||||
image?: string;
|
||||
name?: string;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
|
||||
interface Session extends DefaultSession {
|
||||
user?: Omit<User, "apiSession">;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
interface Session extends DefaultSession {
|
||||
user?: Omit<User, "apiSession">;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
user?: Omit<User, "apiSession">;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
interface JWT {
|
||||
user?: Omit<User, "apiSession">;
|
||||
apiSession?: ApiSession;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
min-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Suspense } from "react";
|
||||
import { LogInWidget } from "../../components/log-in/log-in.widget";
|
||||
import styles from "./log-in.module.scss";
|
||||
|
||||
type LogInWidgetsProps = {};
|
||||
|
||||
export const LogInView: React.FC<LogInWidgetsProps> = ({}) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LogInWidget />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Suspense>
|
||||
<div className={styles.container}>
|
||||
<LogInWidget />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user