feat(auth): Base auth with plugins
- Add fastify to protect routes plugins (requireAuth: true) - Simple Demo to show regular password auth (no 2FA/OTP nor remote auth) - Currently supports: login, logout, signin - OTP workflow should work, not tested - Fixed convention for docker volumes (now all placed in /volumes/<name>)
This commit is contained in:
parent
ddde700494
commit
964fe908a6
17 changed files with 398 additions and 197 deletions
|
|
@ -18,8 +18,8 @@ services:
|
||||||
- '8888:443'
|
- '8888:443'
|
||||||
volumes:
|
volumes:
|
||||||
# if you need to share files with nginx, you do it here.
|
# if you need to share files with nginx, you do it here.
|
||||||
|
- static-volume:/volumes/static
|
||||||
- images-volume:/volumes/icons
|
- images-volume:/volumes/icons
|
||||||
# - static-volume:/volumes/static
|
|
||||||
environment:
|
environment:
|
||||||
# this can stay the same for developpement. This is an alias to `localhost`
|
# this can stay the same for developpement. This is an alias to `localhost`
|
||||||
- NGINX_DOMAIN=local.maix.me
|
- NGINX_DOMAIN=local.maix.me
|
||||||
|
|
@ -38,11 +38,11 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- transcendance-network
|
- transcendance-network
|
||||||
volumes:
|
volumes:
|
||||||
- images-volume:/store
|
- images-volume:/volumes/store
|
||||||
- sqlite-volume:/database
|
- sqlite-volume:/volumes/database
|
||||||
environment:
|
environment:
|
||||||
- USER_ICONS_STORE=/store
|
- USER_ICONS_STORE=/volumes/store
|
||||||
- DATABASE_DIR=/database
|
- DATABASE_DIR=/volumes/database
|
||||||
|
|
||||||
|
|
||||||
###############
|
###############
|
||||||
|
|
@ -53,15 +53,17 @@ services:
|
||||||
context: ./src/
|
context: ./src/
|
||||||
args:
|
args:
|
||||||
- SERVICE=auth
|
- SERVICE=auth
|
||||||
#- EXTRA_FILES=icons/extra
|
- EXTRA_FILES=auth/extra
|
||||||
container_name: auth
|
container_name: auth
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- transcendance-network
|
- transcendance-network
|
||||||
volumes:
|
volumes:
|
||||||
- sqlite-volume:/database
|
- sqlite-volume:/volumes/database
|
||||||
|
- static-volume:/volumes/static
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_DIR=/database
|
- JWT_SECRET=KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JA
|
||||||
|
- DATABASE_DIR=/volumes/database
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
#forward the post request to the microservice
|
#forward the post request to the microservice
|
||||||
location /api/auth/ {
|
location /api/auth/ {
|
||||||
rewrite ^/api/auth/(.*) $1 break;
|
proxy_pass http://auth$uri;
|
||||||
proxy_pass http://auth/$uri;
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /volumes/static/auth/;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,119 @@
|
||||||
import OTP from "otp";
|
import OTP from "otp";
|
||||||
import fastifyJwt from "@fastify/jwt";
|
|
||||||
import fp from 'fastify-plugin'
|
|
||||||
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
||||||
import { Static, Type } from "@sinclair/typebox"
|
|
||||||
import { useDatabase, user } from "@shared/database"
|
|
||||||
import cookie from "@fastify/cookie";
|
import cookie from "@fastify/cookie";
|
||||||
|
import fastifyJwt from "@fastify/jwt";
|
||||||
|
import fp from "fastify-plugin";
|
||||||
|
import { FastifyPluginAsync, preValidationAsyncHookHandler } from "fastify";
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import { UserId } from "@shared/database/mixin/user";
|
||||||
|
import { useDatabase } from "@shared/database";
|
||||||
|
import { makeResponse } from "@shared/utils";
|
||||||
|
|
||||||
|
const kRouteAuthDone = Symbol("shared-route-auth-done");
|
||||||
|
|
||||||
const kRouteAuthDone = Symbol('shared-route-auth-done');
|
type AuthedUser = {
|
||||||
|
id: UserId;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module "fastify" {
|
||||||
export interface FastifyInstance {
|
export interface FastifyInstance {
|
||||||
signJwt: (kind: "auth" | "otp", who: string) => string;
|
signJwt: (kind: "auth" | "otp", who: string) => string;
|
||||||
|
[s: symbol]: boolean;
|
||||||
}
|
}
|
||||||
export interface FastifyRequest {
|
export interface FastifyRequest {
|
||||||
authUser?: user.UserId;
|
authUser?: AuthedUser;
|
||||||
}
|
}
|
||||||
export interface FastifyContextConfig {
|
export interface FastifyContextConfig {
|
||||||
requireAuth?: boolean,
|
requireAuth?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Otp = OTP;
|
export const Otp = OTP;
|
||||||
|
let jwtAdded = false;
|
||||||
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
export const jwtPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
if (jwtAdded) jwtAdded = true;
|
||||||
let env = process.env.JWT_SECRET;
|
let env = process.env.JWT_SECRET;
|
||||||
if (env === undefined || env === null)
|
if (env === undefined || env === null) throw "JWT_SECRET is not defined";
|
||||||
throw "JWT_SECRET is not defined"
|
if (!fastify.hasDecorator("signJwt")) {
|
||||||
|
void fastify.decorate("signJwt", (kind, who) =>
|
||||||
|
fastify.jwt.sign({ kind, who, createdAt: Date.now() }),
|
||||||
|
);
|
||||||
void fastify.register(fastifyJwt, {
|
void fastify.register(fastifyJwt, {
|
||||||
secret: env,
|
secret: env,
|
||||||
decode: { complete: false },
|
decode: { complete: false },
|
||||||
});
|
});
|
||||||
void fastify.decorate("signJwt", (kind, who) => fastify.jwt.sign({ kind, who, createdAt: Date.now() }))
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const JwtType = Type.Object({
|
export const JwtType = Type.Object({
|
||||||
kind: Type.Union([
|
kind: Type.Union([
|
||||||
Type.Const("otp", { description: "the token is only valid for otp call" }),
|
Type.Const("otp", {
|
||||||
Type.Const("auth", { description: "the token is valid for authentication" })
|
description: "the token is only valid for otp call",
|
||||||
|
}),
|
||||||
|
Type.Const("auth", {
|
||||||
|
description: "the token is valid for authentication",
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
who: Type.String({ description: "the login of the user" }),
|
who: Type.String({ description: "the login of the user" }),
|
||||||
createdAt: Type.Integer({ description: "Unix timestamp of when the token as been created at" })
|
createdAt: Type.Integer({
|
||||||
|
description: "Unix timestamp of when the token as been created at",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JwtType = Static<typeof JwtType>;
|
export type JwtType = Static<typeof JwtType>;
|
||||||
|
|
||||||
|
let authAdded = false;
|
||||||
export const authPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
export const authPlugin = fp<FastifyPluginAsync>(async (fastify, _opts) => {
|
||||||
|
if (authAdded) return void console.log("skipping");
|
||||||
|
authAdded = true;
|
||||||
await fastify.register(useDatabase as any, {});
|
await fastify.register(useDatabase as any, {});
|
||||||
await fastify.register(jwtPlugin as any, {});
|
await fastify.register(jwtPlugin as any, {});
|
||||||
await fastify.register(cookie);
|
await fastify.register(cookie);
|
||||||
fastify.addHook('onRoute', (routeOpts) => {
|
if (!fastify.hasRequestDecorator("authUser"))
|
||||||
if (routeOpts.config?.requireAuth) {
|
fastify.decorateRequest("authUser", undefined);
|
||||||
routeOpts.preValidation = [function(req, res) {
|
fastify.addHook("onRoute", (routeOpts) => {
|
||||||
|
if (
|
||||||
|
routeOpts.config?.requireAuth &&
|
||||||
|
!(routeOpts as any)[kRouteAuthDone]
|
||||||
|
) {
|
||||||
|
let f: preValidationAsyncHookHandler = async function(req, res) {
|
||||||
|
try {
|
||||||
if (req.cookies.token === undefined)
|
if (req.cookies.token === undefined)
|
||||||
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
return res
|
||||||
|
.clearCookie("token")
|
||||||
|
.send(
|
||||||
|
JSON.stringify(makeResponse("notLoggedIn", "auth.noCookie")),
|
||||||
|
);
|
||||||
let tok = this.jwt.verify<JwtType>(req.cookies.token);
|
let tok = this.jwt.verify<JwtType>(req.cookies.token);
|
||||||
if (tok.kind != "auth")
|
if (tok.kind != "auth")
|
||||||
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
return res
|
||||||
|
.clearCookie("token")
|
||||||
|
.send(
|
||||||
|
JSON.stringify(makeResponse("notLoggedIn", "auth.invalidKind")),
|
||||||
|
);
|
||||||
let user = this.db.getUserFromName(tok.who);
|
let user = this.db.getUserFromName(tok.who);
|
||||||
if (user === null)
|
if (user === null)
|
||||||
return res.clearCookie("token").send({ kind: "notLoggedIn", msg_key: "" })
|
return res
|
||||||
req.authUser = user.id;
|
.clearCookie("token")
|
||||||
}, ...(routeOpts.preValidation as any || []),];
|
.send(
|
||||||
|
JSON.stringify(makeResponse("notLoggedIn", "auth.noUser")),
|
||||||
|
);
|
||||||
|
req.authUser = { id: user.id, name: tok.who };
|
||||||
|
} catch {
|
||||||
|
return res
|
||||||
|
.clearCookie("token")
|
||||||
|
.send(JSON.stringify(makeResponse("notLoggedIn", "auth.invalid")));
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
})
|
if (!routeOpts.preValidation) {
|
||||||
|
routeOpts.preValidation = [f];
|
||||||
|
} else if (Array.isArray(routeOpts.preValidation)) {
|
||||||
|
routeOpts.preValidation.push(f);
|
||||||
|
} else {
|
||||||
|
routeOpts.preValidation = [routeOpts.preValidation, f];
|
||||||
|
}
|
||||||
|
|
||||||
|
(routeOpts as any)[kRouteAuthDone] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,20 @@ declare module 'fastify' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dbAdded = false;
|
||||||
|
|
||||||
export const useDatabase = fp<FastifyPluginAsync>(async function(
|
export const useDatabase = fp<FastifyPluginAsync>(async function(
|
||||||
f: FastifyInstance,
|
f: FastifyInstance,
|
||||||
_options: {}) {
|
_options: {}) {
|
||||||
|
if (dbAdded)
|
||||||
|
return;
|
||||||
|
dbAdded = true;
|
||||||
let path = process.env.DATABASE_DIR;
|
let path = process.env.DATABASE_DIR;
|
||||||
if (path === null || path === undefined)
|
if (path === null || path === undefined)
|
||||||
throw "env `DATABASE_DIR` not defined";
|
throw "env `DATABASE_DIR` not defined";
|
||||||
f.log.info(`Opening database with path: ${path}/database.db`)
|
f.log.info(`Opening database with path: ${path}/database.db`)
|
||||||
let db: Database = new DbImpl(`${path}/database.db`) as Database;
|
let db: Database = new DbImpl(`${path}/database.db`) as Database;
|
||||||
|
if (!f.hasDecorator("db"))
|
||||||
f.decorate('db', db);
|
f.decorate('db', db);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
58
src/@shared/src/utils/index.ts
Normal file
58
src/@shared/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Represent a message key
|
||||||
|
* Used for translation of text, taken from a prebuilt dictionary
|
||||||
|
* Format: `category.sub.desc`
|
||||||
|
*
|
||||||
|
* @example `login.failure.invalid`
|
||||||
|
* @example `login.failure.missingPassword`
|
||||||
|
* @example `login.failure.missingUser`
|
||||||
|
* @example `signin.success`
|
||||||
|
* @example `pong.you.lost`
|
||||||
|
*/
|
||||||
|
export type MessageKey = string;
|
||||||
|
export type ResponseBase<T = {}> = {
|
||||||
|
kind: string,
|
||||||
|
msg: MessageKey,
|
||||||
|
payload?: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Builds a response from a `kind`, `key` and an arbitrary payload
|
||||||
|
*
|
||||||
|
* * USE THIS FUNCTION TO ALLOW GREPING :) *
|
||||||
|
*
|
||||||
|
* @example makeResponse("failure", "login.failure.invalid")
|
||||||
|
* @example makeResponse("success", "login.success", { token: "supersecrettoken" })
|
||||||
|
*/
|
||||||
|
export function makeResponse<T = {}>(kind: string, key: MessageKey, payload?: T): ResponseBase<T> {
|
||||||
|
console.log(`making response {kind: ${JSON.stringify(kind)}; key: ${JSON.stringify(key)}}`)
|
||||||
|
return { kind, msg: key, payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Create a typebox Type for a response.
|
||||||
|
*
|
||||||
|
* @example typeResponse("failure", ["login.failure.invalid", "login.failure.generic", "login.failure.missingPassword"])
|
||||||
|
* @example typeResponse("otpRequired", "login.otpRequired", { token: Type.String() })
|
||||||
|
* @example typeResponse("success", "login.success", { token: Type.String() })
|
||||||
|
*/
|
||||||
|
export function typeResponse(kind: string, key: MessageKey | MessageKey[], payload?: any): any {
|
||||||
|
let tKey;
|
||||||
|
if (key instanceof Array) {
|
||||||
|
tKey = Type.Union(key.map(l => Type.Const(l)));
|
||||||
|
} else {
|
||||||
|
tKey = Type.Const(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Ty = {
|
||||||
|
kind: Type.Const(kind),
|
||||||
|
msg: tKey,
|
||||||
|
};
|
||||||
|
if (payload !== undefined)
|
||||||
|
Object.assign(Ty, { payload: Type.Object(payload) })
|
||||||
|
|
||||||
|
return Type.Object(Ty)
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,6 @@ ARG EXTRA_FILES=empty
|
||||||
COPY --from=builder /dist /src
|
COPY --from=builder /dist /src
|
||||||
RUN pnpm install --prod --frozen-lockfile;
|
RUN pnpm install --prod --frozen-lockfile;
|
||||||
COPY ${EXTRA_FILES} /extra
|
COPY ${EXTRA_FILES} /extra
|
||||||
RUN echo "${EXTRA_FILES}";
|
|
||||||
ENTRYPOINT [ "/src/entrypoint.sh" ]
|
ENTRYPOINT [ "/src/entrypoint.sh" ]
|
||||||
|
|
||||||
CMD ["node", "/src/run.cjs"]
|
CMD ["node", "/src/run.cjs"]
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ set -e
|
||||||
set -x
|
set -x
|
||||||
# do anything here
|
# do anything here
|
||||||
|
|
||||||
cp -r /extra /files
|
mkdir -p /volumes/static/auth/
|
||||||
|
cp -r /extra/login_demo.html /volumes/static/auth/index.html
|
||||||
|
|
||||||
# run the CMD [ ... ] from the dockerfile
|
# run the CMD [ ... ] from the dockerfile
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
|
||||||
84
src/auth/extra/login_demo.html
Normal file
84
src/auth/extra/login_demo.html
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title> Demo Page For Login :) </title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1> Welcome <span id="t-username"> </span> </h1>
|
||||||
|
|
||||||
|
<input id="i-username" type="text" placeholder="Username"> </input>
|
||||||
|
<input id="i-password" type="text" placeholder="Password"> </input>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<button id="b-login"> Login </button>
|
||||||
|
<br />
|
||||||
|
<button id="b-logout"> Logout </button>
|
||||||
|
<br />
|
||||||
|
<button id="b-signin"> Signin </button>
|
||||||
|
<br />
|
||||||
|
<button id="b-whoami"> Whoami </button>
|
||||||
|
|
||||||
|
<div id="d-response">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
const tUsername = document.querySelector("#t-username")
|
||||||
|
|
||||||
|
const iUsername = document.querySelector("#i-username");
|
||||||
|
const iPassword = document.querySelector("#i-password");
|
||||||
|
|
||||||
|
const bLogin = document.querySelector("#b-login");
|
||||||
|
const bLogout = document.querySelector("#b-logout");
|
||||||
|
const bSignin = document.querySelector("#b-signin");
|
||||||
|
const bWhoami = document.querySelector("#b-whoami");
|
||||||
|
|
||||||
|
const dResponse = document.querySelector("#d-response");
|
||||||
|
|
||||||
|
bWhoami.addEventListener("click", async () => {
|
||||||
|
let username = "";
|
||||||
|
try {
|
||||||
|
let res = await fetch("/api/auth/whoami");
|
||||||
|
const json = await res.json();
|
||||||
|
if (json?.kind === "success")
|
||||||
|
username = json?.payload?.name;
|
||||||
|
else
|
||||||
|
username = `<not logged in: ${json.msg}>`
|
||||||
|
} catch {
|
||||||
|
username = `<not logged in: threw>`
|
||||||
|
}
|
||||||
|
tUsername.innerText = username;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
bLogin.addEventListener("click", async () => {
|
||||||
|
const name = iUsername.value;
|
||||||
|
const password = iPassword.value;
|
||||||
|
|
||||||
|
let res = await fetch("/api/auth/login", {method: "POST", body: JSON.stringify({name, password}), headers});
|
||||||
|
let j = await res.json();
|
||||||
|
if (j?.payload?.token)
|
||||||
|
document.cookie = `token=${j?.payload?.token}`;
|
||||||
|
dResponse.innerText = JSON.stringify(j, space=4);
|
||||||
|
})
|
||||||
|
bLogout.addEventListener("click", async () => {
|
||||||
|
let res = await fetch("/api/auth/logout", { method: "POST" });
|
||||||
|
dResponse.innerText = `done - status:${res.status}`;
|
||||||
|
})
|
||||||
|
bSignin.addEventListener("click", async () => {
|
||||||
|
const name = iUsername.value;
|
||||||
|
const password = iPassword.value;
|
||||||
|
|
||||||
|
let res = await fetch("/api/auth/signin", {method: "POST", body: JSON.stringify({name, password}), headers});
|
||||||
|
let j = await res.json();
|
||||||
|
if (j?.payload?.token)
|
||||||
|
document.cookie = `token=${j?.payload?.token};`;
|
||||||
|
dResponse.innerText = JSON.stringify(j, space=4);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-cli": "^7.4.0",
|
"fastify-cli": "^7.4.0",
|
||||||
"fastify-plugin": "^5.0.0",
|
"fastify-plugin": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ const app: FastifyPluginAsync = async (
|
||||||
fastify,
|
fastify,
|
||||||
opts
|
opts
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
|
await fastify.register(db.useDatabase as any, {})
|
||||||
|
await fastify.register(auth.jwtPlugin as any, {})
|
||||||
|
await fastify.register(auth.authPlugin as any, {})
|
||||||
|
|
||||||
// Place here your custom code!
|
// Place here your custom code!
|
||||||
for (const plugin of Object.values(plugins)) {
|
for (const plugin of Object.values(plugins)) {
|
||||||
void fastify.register(plugin as any, {});
|
void fastify.register(plugin as any, {});
|
||||||
|
|
@ -31,20 +35,8 @@ const app: FastifyPluginAsync = async (
|
||||||
void fastify.register(route as any, {});
|
void fastify.register(route as any, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
await fastify.register(db.useDatabase as any, {})
|
|
||||||
await fastify.register(auth.jwtPlugin as any, {})
|
|
||||||
void fastify.register(fastifyFormBody, {})
|
void fastify.register(fastifyFormBody, {})
|
||||||
void fastify.register(fastifyMultipart, {})
|
void fastify.register(fastifyMultipart, {})
|
||||||
console.log(fastify.db.getUser(0 as any));
|
|
||||||
|
|
||||||
// The use of fastify-plugin is required to be able
|
|
||||||
// to export the decorators to the outer scope
|
|
||||||
void fastify.register(fp(async (fastify) => {
|
|
||||||
const image_store = process.env.USER_ICONS_STORE ?? "/tmp/icons";
|
|
||||||
fastify.decorate('image_store', image_store)
|
|
||||||
await mkdir(fastify.image_store, { recursive: true })
|
|
||||||
}))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
|
||||||
|
|
@ -3,74 +3,56 @@ import { FastifyPluginAsync } from "fastify";
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
import { user as userDb } from "@shared/database";
|
import { user as userDb } from "@shared/database";
|
||||||
import type { } from "@shared/auth";
|
import type { } from "@shared/auth";
|
||||||
|
import { typeResponse, makeResponse } from "@shared/utils"
|
||||||
|
|
||||||
export const LoginReq = Type.Object({
|
export const LoginReq = Type.Object({
|
||||||
name: Type.String(),
|
name: Type.String(),
|
||||||
password: Type.String({ minLength: 8, maxLength: 32 }),
|
password: Type.String(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LoginReq = Static<typeof LoginReq>;
|
export type LoginReq = Static<typeof LoginReq>;
|
||||||
|
|
||||||
|
|
||||||
export const LoginRes = Type.Union([
|
export const LoginRes = Type.Union([
|
||||||
Type.Object({
|
typeResponse("failed", ["login.failed.generic", "login.failed.invalid"]),
|
||||||
kind: Type.Const("failed"),
|
typeResponse("otpRequired", "login.otpRequired", { token: Type.String({ description: "JWT to send with the OTP to finish login" }) }),
|
||||||
msg_key: Type.Union([
|
typeResponse("success", "login.success", { token: Type.String({ description: "JWT that represent a logged in user" }) }),
|
||||||
Type.Const("login.failed.generic"),
|
|
||||||
Type.Const("login.failed.invalid"),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
kind: Type.Const("otpRequired"),
|
|
||||||
msg_key: Type.Const("login.otpRequired"),
|
|
||||||
token: Type.String({
|
|
||||||
description: "Code to send with the OTP to finish login",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
kind: Type.Const("success"),
|
|
||||||
msg_key: Type.Const("login.success"),
|
|
||||||
token: Type.String({ description: "The JWT token" }),
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
export type LoginRes = Static<typeof LoginRes>;
|
export type LoginRes = Static<typeof LoginRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
|
fastify.post<{ Body: LoginReq; Response: LoginRes }>(
|
||||||
"/login",
|
"/api/auth/login",
|
||||||
{
|
{ schema: { body: LoginReq, response: { "2xx": LoginRes } }, },
|
||||||
schema: {
|
|
||||||
body: LoginReq,
|
|
||||||
response: { "2xx": LoginRes },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async function(req, res) {
|
async function(req, res) {
|
||||||
try {
|
try {
|
||||||
let { name, password } = req.body;
|
let { name, password } = req.body;
|
||||||
|
console.log("HELLOO FDP");
|
||||||
let user = this.db.getUserFromName(name);
|
let user = this.db.getUserFromName(name);
|
||||||
|
|
||||||
// does the user exist
|
// does the user exist
|
||||||
// does it have a password setup ?
|
// does it have a password setup ?
|
||||||
if (user === null || user.password === null)
|
if (user === null || user.password === null)
|
||||||
return { kind: "failed", msg_key: "login.failed.invalid" };
|
return makeResponse("failed", "login.failed.invalid");
|
||||||
|
|
||||||
// does the password he provided match the one we have
|
// does the password he provided match the one we have
|
||||||
if (!(await userDb.verifyUserPassword(user, password)))
|
if (!(await userDb.verifyUserPassword(user, password)))
|
||||||
return { kind: "failed", msg_key: "login.failed.invalid" };
|
return makeResponse("failed", "login.failed.invalid");
|
||||||
|
|
||||||
// does the user has 2FA up ?
|
// does the user has 2FA up ?
|
||||||
if (user.otp !== null) {
|
if (user.otp !== undefined) {
|
||||||
|
console.log(user);
|
||||||
// yes -> we ask them to fill it,
|
// yes -> we ask them to fill it,
|
||||||
// send them somehting to verify that they indeed passed throught the user+password phase
|
// send them somehting to verify that they indeed passed throught the user+password phase
|
||||||
return { kind: "otpRequired", msg_key: "login.otpRequired", token: this.signJwt("otp", user.name) };
|
return makeResponse("otpRequired", "login.otpRequired", { token: this.signJwt("otp", user.name) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// every check has been passed, they are now logged in, using this token to say who they are...
|
// every check has been passed, they are now logged in, using this token to say who they are...
|
||||||
return { kind: "success", msg_key: "login.success", token: this.signJwt("auth", user.name) }
|
return makeResponse("success", "login.success", { token: this.signJwt("auth", user.name) });
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return { kind: "failed", msg_key: "login.failed.generic" };
|
return makeResponse("failed", "login.failed.generic");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
12
src/auth/src/routes/logout.ts
Normal file
12
src/auth/src/routes/logout.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { FastifyPluginAsync } from "fastify";
|
||||||
|
|
||||||
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
|
fastify.post(
|
||||||
|
"/api/auth/logout",
|
||||||
|
async function(req, res) {
|
||||||
|
return res.clearCookie("token").send("bye :(")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default route;
|
||||||
|
|
@ -2,37 +2,27 @@ import { FastifyPluginAsync } from "fastify";
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
import { JwtType, Otp } from "@shared/auth";
|
import { JwtType, Otp } from "@shared/auth";
|
||||||
|
import { typeResponse, makeResponse } from "@shared/utils";
|
||||||
|
|
||||||
export const OtpReq = Type.Object({
|
const OtpReq = Type.Object({
|
||||||
token: Type.String({ description: "The token given at the login phase" }),
|
token: Type.String({ description: "The token given at the login phase" }),
|
||||||
code: Type.String({ description: "The OTP given by the user" }),
|
code: Type.String({ description: "The OTP given by the user" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type OtpReq = Static<typeof OtpReq>;
|
type OtpReq = Static<typeof OtpReq>;
|
||||||
|
|
||||||
export const OtpRes = Type.Union([
|
const OtpRes = Type.Union([
|
||||||
Type.Object({
|
typeResponse("failed", ["otp.failed.generic", "otp.failed.invalid", "otp.failed.timeout"]),
|
||||||
kind: Type.Const("failed"),
|
typeResponse("success", "otp.success", { token: Type.String({ description: "the JWT Token" }) }),
|
||||||
msg_key: Type.Union([
|
|
||||||
Type.Const("otp.failed.generic"),
|
|
||||||
Type.Const("otp.failed.invalid"),
|
|
||||||
Type.Const("otp.failed.timeout"),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
kind: Type.Const("success"),
|
|
||||||
msg_key: Type.Const("otp.success"),
|
|
||||||
token: Type.String({ description: "The JWT token" }),
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type OtpRes = Static<typeof OtpRes>;
|
type OtpRes = Static<typeof OtpRes>;
|
||||||
|
|
||||||
const OTP_TOKEN_TIMEOUT_SEC = 120;
|
const OTP_TOKEN_TIMEOUT_SEC = 120;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
fastify.get<{ Body: OtpReq }>(
|
fastify.post<{ Body: OtpReq }>(
|
||||||
"/whoami",
|
"/api/auth/otp",
|
||||||
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
|
{ schema: { body: OtpReq, response: { "2xx": OtpRes } } },
|
||||||
async function(req, res) {
|
async function(req, res) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -43,18 +33,18 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
// is the jwt a valid `otp` jwt ?
|
// is the jwt a valid `otp` jwt ?
|
||||||
if (dJwt.kind != "otp")
|
if (dJwt.kind != "otp")
|
||||||
// no ? fuck off then
|
// no ? fuck off then
|
||||||
return { kind: "failed", msg_key: "otp.failed.invalid" };
|
return makeResponse("failed", "otp.failed.invalid");
|
||||||
// is it too old ?
|
// is it too old ?
|
||||||
if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 > Date.now())
|
if (dJwt.createdAt + OTP_TOKEN_TIMEOUT_SEC * 1000 > Date.now())
|
||||||
// yes ? fuck off then, redo the password
|
// yes ? fuck off then, redo the password
|
||||||
return { kind: "failed", msg_key: "otp.failed.timeout" };
|
return makeResponse("failed", "otp.failed.timeout");
|
||||||
|
|
||||||
// get the Otp sercret from the db
|
// get the Otp sercret from the db
|
||||||
let otpSecret = this.db.getUserFromName(dJwt.who)?.otp;
|
let otpSecret = this.db.getUserFromName(dJwt.who)?.otp;
|
||||||
if (otpSecret === null)
|
if (otpSecret === null)
|
||||||
// oops, either no user, or user without otpSecret
|
// oops, either no user, or user without otpSecret
|
||||||
// fuck off
|
// fuck off
|
||||||
return { kind: "failed", msg_key: "otp.failed.invalid" };
|
return makeResponse("failed", "otp.failed.invalid");
|
||||||
|
|
||||||
// good lets now verify the token you gave us is the correct one...
|
// good lets now verify the token you gave us is the correct one...
|
||||||
let otpHandle = new Otp({ secret: otpSecret });
|
let otpHandle = new Otp({ secret: otpSecret });
|
||||||
|
|
@ -71,13 +61,9 @@ const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
if (tokens.some((c) => c === code))
|
if (tokens.some((c) => c === code))
|
||||||
// they do !
|
// they do !
|
||||||
// gg you are now logged in !
|
// gg you are now logged in !
|
||||||
return {
|
return makeResponse("success", "otp.success", { token: this.signJwt("auth", dJwt.who) });
|
||||||
kind: "success",
|
|
||||||
msg_key: "otp.success",
|
|
||||||
token: this.signJwt("auth", dJwt.who),
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return { kind: "failed", msg_key: "otp.failed.generic" };
|
return makeResponse("failed", "otp.failed.generic");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,63 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from "fastify";
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import { typeResponse, makeResponse } from "@shared/utils";
|
||||||
|
|
||||||
const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/;
|
const USERNAME_CHECK: RegExp = /^[a-zA-Z\_0-9]+$/;
|
||||||
|
|
||||||
export const SignInReq = Type.Object({
|
const SignInReq = Type.Object({
|
||||||
name: Type.String(),
|
name: Type.String(),
|
||||||
password: Type.String({ minLength: 8, maxLength: 32 }),
|
password: Type.String(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SignInReq = Static<typeof SignInReq>;
|
type SignInReq = Static<typeof SignInReq>;
|
||||||
|
|
||||||
export const SignInRes = Type.Union([
|
const SignInRes = Type.Union([
|
||||||
Type.Object({
|
typeResponse("failed", [
|
||||||
kind: Type.Const("failed"),
|
"signin.failed.generic",
|
||||||
msg_key: Type.Union([
|
"signin.failed.username.existing",
|
||||||
Type.Const("signin.failed.generic"),
|
"signin.failed.username.toolong",
|
||||||
Type.Const("signin.failed.username.existing"),
|
"signin.failed.username.tooshort",
|
||||||
Type.Const("signin.failed.username.toolong"),
|
"signin.failed.username.invalid",
|
||||||
Type.Const("signin.failed.username.tooshort"),
|
"signin.failed.password.toolong",
|
||||||
Type.Const("signin.failed.username.invalid"),
|
"signin.failed.password.tooshort",
|
||||||
Type.Const("signin.failed.password.toolong"),
|
"signin.failed.password.invalid",
|
||||||
Type.Const("signin.failed.password.tooshort"),
|
|
||||||
Type.Const("signin.failed.password.invalid"),
|
|
||||||
]),
|
]),
|
||||||
}),
|
typeResponse("success", "signin.success", { token: Type.String({ description: "the JWT token" }) }),
|
||||||
Type.Object({
|
])
|
||||||
kind: Type.Const("sucess"),
|
|
||||||
msg_key: Type.Const("signin.sucess"),
|
|
||||||
token: Type.String({ description: "The JWT token" }),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type SignInRes = Static<typeof SignInRes>;
|
type SignInRes = Static<typeof SignInRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
fastify.post<{ Body: SignInReq; Response: SignInRes }>(
|
fastify.post<{ Body: SignInReq }>(
|
||||||
"/signin",
|
"/api/auth/signin",
|
||||||
{ schema: { body: SignInReq, response: { "2xx": SignInRes } } },
|
{ schema: { body: SignInReq, response: { "200": SignInRes, "5xx": Type.Object({}) } }, },
|
||||||
async function(req, res) {
|
async function(req, res) {
|
||||||
const { name, password } = req.body;
|
const { name, password } = req.body;
|
||||||
|
|
||||||
if (name.length < 4)
|
if (name.length < 4)
|
||||||
return {
|
return makeResponse("failed", "signin.failed.username.tooshort");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.username.tooshort",
|
|
||||||
};
|
|
||||||
if (name.length > 32)
|
if (name.length > 32)
|
||||||
return {
|
return makeResponse("failed", "signin.failed.username.toolong");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.username.toolong",
|
|
||||||
};
|
|
||||||
if (!USERNAME_CHECK.test(name))
|
if (!USERNAME_CHECK.test(name))
|
||||||
return {
|
return makeResponse("failed", "signin.failed.username.invalid");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.username.invalid",
|
|
||||||
};
|
|
||||||
// username if good now :)
|
// username if good now :)
|
||||||
|
|
||||||
if (password.length < 8)
|
if (password.length < 8)
|
||||||
return {
|
return makeResponse("failed", "signin.failed.password.tooshort");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.password.tooshort",
|
|
||||||
};
|
|
||||||
if (password.length > 64)
|
if (password.length > 64)
|
||||||
return {
|
return makeResponse("failed", "signin.failed.password.toolong");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.password.toolong",
|
|
||||||
};
|
|
||||||
// password is good too !
|
// password is good too !
|
||||||
|
|
||||||
if (this.db.getUserFromName(name) !== null)
|
if (this.db.getUserFromName(name) !== null)
|
||||||
return {
|
return makeResponse("failed", "signin.failed.username.existing");
|
||||||
kind: "failed",
|
|
||||||
msg_key: "signin.failed.username.existing",
|
|
||||||
};
|
|
||||||
let u = await this.db.createUser(name, password);
|
let u = await this.db.createUser(name, password);
|
||||||
if (u === null)
|
if (u === null)
|
||||||
return { kind: "failed", msg_key: "signin.failed.generic" };
|
return makeResponse("failed", "signin.failed.generic");
|
||||||
|
|
||||||
// every check has been passed, they are now logged in, using this token to say who they are...
|
// every check has been passed, they are now logged in, using this token to say who they are...
|
||||||
let userToken = this.jwt.sign({
|
let userToken = this.signJwt('auth', u.name);
|
||||||
kind: "auth",
|
return makeResponse("success", "signin.success", { token: userToken });
|
||||||
user: u.name,
|
|
||||||
createAt: Date.now() / 1000,
|
|
||||||
});
|
|
||||||
let out = {
|
|
||||||
kind: "success",
|
|
||||||
msg_key: "login.success",
|
|
||||||
token: userToken,
|
|
||||||
};
|
|
||||||
console.log(out)
|
|
||||||
return out;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { FastifyPluginAsync } from "fastify";
|
import { FastifyPluginAsync } from "fastify";
|
||||||
|
|
||||||
import { Static, Type } from "@sinclair/typebox";
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import { makeResponse, typeResponse } from "@shared/utils"
|
||||||
|
|
||||||
|
|
||||||
export const WhoAmIRes = Type.String({ description: "username" });
|
export const WhoAmIRes = typeResponse("success", "whoami.success", { name: Type.String() });
|
||||||
|
|
||||||
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
export type WhoAmIRes = Static<typeof WhoAmIRes>;
|
||||||
|
|
||||||
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/whoami",
|
"/api/auth/whoami",
|
||||||
{ schema: { response: { "2xx": WhoAmIRes } } },
|
{ schema: { response: { "2xx": WhoAmIRes } }, config: { requireAuth: true } },
|
||||||
async function(req, res) {
|
async function(req, res) {
|
||||||
return "yes";
|
return makeResponse("success", "whoami.success", { name: req.authUser?.name })
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
64
src/package-lock.json
generated
64
src/package-lock.json
generated
|
|
@ -24,13 +24,16 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/jwt": "^9.1.0",
|
"@fastify/jwt": "^9.1.0",
|
||||||
|
"@sinclair/typebox": "^0.34.40",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
"joi": "^18.0.0",
|
"joi": "^18.0.0",
|
||||||
|
"otp": "^1.1.2",
|
||||||
"uuidv7": "^1.0.2"
|
"uuidv7": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -51,7 +54,6 @@
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"fastify-cli": "^7.4.0",
|
"fastify-cli": "^7.4.0",
|
||||||
"fastify-plugin": "^5.0.0",
|
"fastify-plugin": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -594,6 +596,26 @@
|
||||||
"integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==",
|
"integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/cookie": {
|
||||||
|
"version": "11.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
||||||
|
"integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.0",
|
||||||
|
"fastify-plugin": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/deepmerge": {
|
"node_modules/@fastify/deepmerge": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-2.0.2.tgz",
|
||||||
|
|
@ -1997,6 +2019,15 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/charenc": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
@ -2112,6 +2143,15 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypt": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dateformat": {
|
"node_modules/dateformat": {
|
||||||
"version": "4.6.3",
|
"version": "4.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||||
|
|
@ -3118,6 +3158,15 @@
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/otp": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/otp/-/otp-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-VoueTSCMNTCYyHMGkNfndkFSXNv+iyEJ8D1/zD5G0Rd/QUHWozySmezRWKdVAhxcvmL3e5qwhEJBH/JF9MyE+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sha1": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
|
@ -3691,6 +3740,19 @@
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sha1": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"charenc": ">= 0.0.1",
|
||||||
|
"crypt": ">= 0.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shared": {
|
"node_modules/shared": {
|
||||||
"resolved": "@shared",
|
"resolved": "@shared",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,7 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@shared/auth": ["@shared/src/auth"],
|
"@shared/*": ["@shared/src/*"]
|
||||||
"@shared/auth/*": ["@shared/src/auth/*"],
|
|
||||||
"@shared/database": ["@shared/src/database"],
|
|
||||||
"@shared/database/*": ["@shared/src/database/*"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue