Compare commits

...

11 Commits

Author SHA1 Message Date
72d310ccfa
build: packages 2024-04-23 13:54:18 +02:00
cf548890fc
build: 🔧 tsconfig 2024-04-23 13:51:59 +02:00
580a4a4320
build: 🔧 biomejs config 2024-04-23 13:51:13 +02:00
2cb9c142c4
feat: 🥚 .env 2024-04-23 13:50:41 +02:00
92e0fd2a68
chore: gitignore 2024-04-23 13:48:21 +02:00
17cccc0c9f
fix: ✏️ Jwt type import 2024-04-23 13:47:40 +02:00
9f8582c412
feat: 🎉 App endpoint 2024-04-23 13:46:13 +02:00
ad0f30876e
feat: MySQL Service 2024-04-23 13:44:58 +02:00
db40b772f1
feat: 🚀 Jwt Service 2024-04-23 13:44:13 +02:00
ddd9a2fef4
build: 🔧 JetBrains config 2024-04-23 13:43:33 +02:00
c12439d1ed
feat: DbUserData interface 2024-04-23 13:42:55 +02:00
20 changed files with 552 additions and 0 deletions

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
HASH_SECRET=''
JWT_SECRET=''
PROJECT_NAME=''
MYSQL_HOST=''
MYSQL_PORT=''
MYSQL_USERNAME=''
MYSQL_PASS=''
MYSQL_DATABASE=''

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
pnpm-lock.yaml

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="82" name="TypeScript" />
</Languages>
</inspection_tool>
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

25
.idea/jsonSchemas.xml generated Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JsonSchemaMappingsProjectConfiguration">
<state>
<map>
<entry key="TSConfig">
<value>
<SchemaInfo>
<option name="name" value="TSConfig" />
<option name="relativePathToSchema" value="http://json.schemastore.org/tsconfig" />
<option name="applicationDefined" value="true" />
<option name="patterns">
<list>
<Item>
<option name="path" value="tsconfig.json" />
</Item>
</list>
</option>
</SchemaInfo>
</value>
</entry>
</map>
</state>
</component>
</project>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/template-express.iml" filepath="$PROJECT_DIR$/.idea/template-express.iml" />
</modules>
</component>
</project>

12
.idea/template-express.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

24
biome.json Normal file
View File

@ -0,0 +1,24 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"performance": {
"recommended": true,
"noDelete": "off"
},
"complexity": {
"useLiteralKeys": "off"
}
}
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 15
}
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "brief-05-back",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"keywords": [],
"author": "Mathis HERRIOT",
"license": "MIT",
"dependencies": {
"@node-rs/argon2": "^1.8.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.19.2",
"express-validator": "^7.0.1",
"express-xss-sanitizer": "^1.2.0",
"jose": "^5.2.4",
"morgan": "^1.10.0",
"mysql2": "^3.9.7",
"tslog": "^4.9.2"
},
"devDependencies": {
"@biomejs/biome": "^1.7.0",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.12.7"
}
}

27
src/app.ts Normal file
View File

@ -0,0 +1,27 @@
import express, { type Express } from 'express';
import cors from 'cors';
import compression from 'compression';
import {Logger} from "tslog";
const logger = new Logger({ name: "App" });
const app: Express = express();
// enable cors
app.use(cors());
app.options('*', cors());
// parse json request body
app.use(express.json());
// parse urlencoded request body
app.use(express.urlencoded({ extended: true }));
// gzip compression
app.use(compression())
//app.use('/auth', AuthRoutes)
//app.listen(3333)
logger.info('Server is running !')

View File

@ -0,0 +1,23 @@
interface DbUserData {
id?: string;
username: string;
displayName: string;
firstName: string;
lastName: string;
email: string;
passwordHash: string;
isAdmin: boolean;
isDisabled: boolean;
resetPasswordToken?: string;
resetPasswordExpires?: Date;
dob: Date;
gdpr: Date;
iat: Date;
uat: Date;
}
export default DbUserData

1
src/interfaces/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './UserData'

2
src/services/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './jwt.service';
export * from './mysql.service'

View File

@ -0,0 +1,60 @@
import Jose, {type JWTHeaderParameters, type JWTPayload} from "jose";
import {Logger} from "tslog";
const logger = new Logger({ name: "JwtService" });
/**
* Verify a JWT token.
*
* @param {string | Uint8Array} jwt
* - The JWT token to verify.
* @returns {Promise<null | object>}
* - The payload of the verified JWT token or null if verification fails.
*/
async function JwtVerifyService(jwt: string | Uint8Array): Promise<null | object> {
try {
const result = await Jose.jwtVerify(
jwt,
new TextEncoder()
.encode(`${process.env["JWT_SECRET"]}`),
{
})
return result.payload;
} catch (error) {
logger.error(error)
return null
}
}
/**
* Asynchronously signs a JWT token using the provided payload, header, expiration time, and audience.
*
* @param {JWTPayload} payload
* - The payload data to include in the JWT token.
* @param {JWTHeaderParameters} pHeader
* - The protected header parameters for the JWT token.
* @param {string | number | Date} expTime
* - The expiration time for the JWT token. (Can be expressed with '1d', '1mo'...)
* @param {string | string[]} audience
* - The intended audience for the JWT token.
*
* @returns {Promise<string>}
* - A promise that resolves with the signed JWT token.
*/
async function JwtSignService(payload: JWTPayload, pHeader: JWTHeaderParameters, expTime: string | number | Date, audience: string | string[]): Promise<string> {
return await new Jose.SignJWT(payload)
.setProtectedHeader(pHeader)
.setIssuedAt(new Date())
.setIssuer(`${process.env["JWT_SECRET"]} - Mathis HERRIOT`)
.setAudience(audience)
.setExpirationTime(expTime)
.sign(new TextEncoder().encode(`${process.env["JWT_SECRET"]}`))
}
const JwtService = {
verify: JwtVerifyService,
sign: JwtSignService
}
export default JwtService

View File

@ -0,0 +1,234 @@
import mysql, {type Connection, type ConnectionOptions} from 'mysql2';
import {Logger} from "tslog";
// biome-ignore lint/style/useImportType: <explanation>
import DbUserData from "@interfaces/UserData";
const access: ConnectionOptions = {
host: `${process.env["MYSQL_HOST"]}`,
port: Number.parseInt(`${process.env["MYSQL_PORT"]}`),
user: `${process.env["MYSQL_USER"]}`,
database: `${process.env["MYSQL_USER"]}`,
password: `${process.env["MYSQL_PASS"]}`
};
class MySqlHandler {
private readonly handlerName: string;
private Logger: Logger<unknown>
private Connection: Connection;
constructor(handlerName?: string) {
this.handlerName = handlerName || 'Unknown';
this.Logger = new Logger({ name: `DB>> ${this.handlerName}` });
this.Connection = mysql.createConnection(access);
this.Connection.connect((err) => {
if (err) {
this.Logger.error(`Error connecting to MySQL: ${err}`);
throw new Error()
}
this.Logger.info(`Connected to MySQL database (${access.database})`);
});
}
closeConnection() {
this.Connection.end();
};
query(queryString: string) {
return new Promise((resolve, reject) => {
this.Connection.query(queryString, (err, results) => {
if (err) {
this.Logger.error(`Error executing query: ${err}`);
reject(err);
} else {
resolve(results);
}
});
});
}
execute(queryString: string, values: Array<string | boolean | Date | number>): Promise<unknown> {
return new Promise((resolve, reject) => {
this.Connection.execute(queryString, values, (err: mysql.QueryError | null, results: mysql.QueryResult) => {
if (err) {
this.Logger.error(`Error executing query: ${err}`);
reject(err);
} else {
resolve(results);
}
});
});
}
/**
* Unprepare a previously prepared SQL query.
*
* @param {string} queryString
* - The SQL query string to unprepare.
* @return {Promise}
* - A promise that resolves if the unprepare operation is successful,
* or rejects with an error if there was an error unpreparing the query.
*/
unprepare(queryString: string): Promise<unknown> {
return new Promise((resolve, reject) => {
try {
resolve(this.Connection.unprepare(queryString));
} catch (err) {
reject(err)
}
});
}
}
const MySqlService = {
Handler : MySqlHandler,
User: {
insert(handler: MySqlHandler, userData: DbUserData) {
return new Promise((resolve, reject) => {
const _now = new Date()
const _sql = "INSERT INTO `users`(`username`, `displayName`, `firstName`, `lastName`, `email`, `passwordHash`, `isAdmin`, `isDisabled`, `dob`, `gdpr`, `iat`, `uat`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
const _values = [
userData.username,
userData.displayName,
userData.firstName,
userData.lastName,
userData.email,
userData.passwordHash,
userData.isAdmin,
userData.isDisabled,
userData.dob,
userData.gdpr,
_now,
_now
]
try {
resolve(handler.execute(_sql, _values))
} catch (err: unknown) {
reject(err as Error);
}
})
},
update(handler: MySqlHandler, userData: DbUserData) {
return new Promise((resolve, reject) => {
//@ts-ignore
const _t = `
${userData.username ? "`username` = ?," : null}
${userData.displayName ? "`displayName` = ?," : null}
${userData.firstName ? "`firstName` = ?," : null}`
const __sql = "UPDATE `users` SET `lastName` = ?, `email` = ?, `passwordHash` = ?, `isAdmin` = ?, `isDisabled` = ?, `dob` = ?, `gdpr` = ? WHERE `id` = ?";
const __values = [
userData.username,
userData.displayName,
userData.firstName,
userData.lastName,
userData.email,
userData.passwordHash,
userData.isAdmin,
userData.isDisabled,
userData.dob,
userData.gdpr
];
try {
resolve(handler.execute(__sql, __values));
} catch (err: unknown) {
reject(err as Error);
}
});
},
getById(handler: MySqlHandler, userId: string): Promise<DbUserData> {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `id` = ?";
const _values = [userId];
try {
resolve(handler.execute(_sql, _values) as unknown as DbUserData);
} catch (err: unknown) {
reject(err as Error);
}
});
},
getAll(handler: MySqlHandler): Promise<Array<DbUserData>> {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users`";
try {
return resolve(handler.query(_sql) as unknown as Array<DbUserData>);
} catch (err: unknown) {
return reject();
}
});
},
getByUsername(handler: MySqlHandler, username: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `username` = ?";
const _values = [username];
try {
resolve(handler.execute(_sql, _values));
} catch (err: unknown) {
reject(err as Error);
}
});
},
getByEmail(handler: MySqlHandler, email: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `email` = ?";
const _values = [email];
try {
resolve(handler.execute(_sql, _values));
} catch (err: unknown) {
reject(err as Error);
}
});
},
getByDisplayName(handler: MySqlHandler, displayName: string) {
return new Promise((resolve, reject) => {
const _sql = "SELECT * FROM `users` WHERE `displayName` = ?";
const _values = [displayName];
try {
resolve(handler.execute(_sql, _values));
} catch (err: unknown) {
reject(err as Error);
}
});
},
getAdminStateForId(handler: MySqlHandler, userId: string) : Promise<boolean> {
return new Promise((resolve, reject) => {
const _sql = "SELECT `isAdmin` FROM `users` WHERE `id` = ?";
const _values = [userId];
try {
const isAdmin = handler.execute(_sql, _values)
isAdmin.then((result) => {
if (result !== true) return resolve(false);
return resolve(true)
});
} catch (err: unknown) {
reject(err as Error);
}
});
},
delete(handler: MySqlHandler, userId: string) {
return new Promise((resolve, reject) => {
const _sql = "DELETE FROM `users` WHERE `id` = ?";
const _values = [userId];
try {
resolve(handler.execute(_sql, _values));
} catch (err: unknown) {
reject(err as Error);
}
});
}
},
}
export default MySqlService

58
tsconfig.json Normal file
View File

@ -0,0 +1,58 @@
{
"compilerOptions": {
"target": "es2017",
"module": "es6",
"rootDir": "./src",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"@services/*": [
"src/services/*"
],
"@controllers/*": [
"src/controllers/*"
],
"@routes/*": [
"src/routes/*"
],
"@utils/*": [
"src/utils/*"
],
"@interfaces/*": [
"src/interfaces/*"
],
"@validators/*": [
"src/validators/*"
]
},
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"removeComments": false,
"noEmitOnError": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": true,
"allowUnreachableCode": true,
"skipLibCheck": true
},
}