Rework authentication
This commit is contained in:
parent
a85330e8cf
commit
c07d33bcc9
17 changed files with 317 additions and 128 deletions
1
env
1
env
|
|
@ -1,2 +1,3 @@
|
||||||
DATABASE_URL="postgresql://tetheruser:password@localhost:5432/tetherdb"
|
DATABASE_URL="postgresql://tetheruser:password@localhost:5432/tetherdb"
|
||||||
JWT_SECRET=""
|
JWT_SECRET=""
|
||||||
|
COOKIE_SECRET=""
|
||||||
|
|
|
||||||
62
package-lock.json
generated
62
package-lock.json
generated
|
|
@ -1,14 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.2.0",
|
"version": "0.3.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.2.0",
|
"version": "0.3.4",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
"@fastify/cors": "^11.2.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
|
|
@ -134,6 +136,46 @@
|
||||||
"fast-uri": "^3.0.0"
|
"fast-uri": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/cors": {
|
||||||
|
"version": "11.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
|
||||||
|
"integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fastify-plugin": "^5.0.0",
|
||||||
|
"toad-cache": "^3.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/error": {
|
"node_modules/@fastify/error": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
||||||
|
|
@ -1066,6 +1108,22 @@
|
||||||
"toad-cache": "^3.7.0"
|
"toad-cache": "^3.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fastify-plugin": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.3.4",
|
"version": "0.3.5",
|
||||||
"description": "Communication server using the Nexlink protocol",
|
"description": "Communication server using the Nexlink protocol",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
"@fastify/cors": "^11.2.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `token` on the `Session` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `cookie` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" DROP COLUMN "token",
|
||||||
|
ADD COLUMN "cookie" TEXT NOT NULL;
|
||||||
|
|
@ -59,7 +59,7 @@ model Session {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
token String
|
cookie String
|
||||||
creationDate DateTime @default(now())
|
creationDate DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||||
import type {
|
import type {
|
||||||
ILoginRequest,
|
IPostLoginRequest,
|
||||||
IRegisterResponseError,
|
IPostRegisterResponseError,
|
||||||
IRegisterResponseSuccess,
|
IPostRegisterResponseSuccess,
|
||||||
IRegisterRequest,
|
IPostRegisterRequest,
|
||||||
ILoginResponseError,
|
IPostLoginResponseError,
|
||||||
ILoginResponseSuccess,
|
IPostLoginResponseSuccess,
|
||||||
|
IGetRefreshResponseError,
|
||||||
|
IGetRefreshResponseSuccess,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import { loginUser, registerUser } from "../../services/auth/auth.js";
|
import {
|
||||||
|
loginUser,
|
||||||
|
refreshSession,
|
||||||
|
registerUser,
|
||||||
|
} from "../../services/auth/auth.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
|
const postRegister = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { username, password, email } = request.body as IRegisterRequest;
|
const { username, password, email } = request.body as IPostRegisterRequest;
|
||||||
|
|
||||||
const newUser = await registerUser({
|
const newUser = await registerUser({
|
||||||
username: username,
|
username: username,
|
||||||
|
|
@ -20,20 +26,21 @@ const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
|
reply.status(409);
|
||||||
return {
|
return {
|
||||||
error: API_ERROR.USER_ALREADY_EXISTS,
|
error: API_ERROR.USER_ALREADY_EXISTS,
|
||||||
} as IRegisterResponseError;
|
} as IPostRegisterResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newUser.id,
|
id: newUser.id,
|
||||||
username: newUser.username,
|
username: newUser.username,
|
||||||
registerDate: newUser.registerDate?.getTime(),
|
registerDate: newUser.registerDate?.getTime(),
|
||||||
} as IRegisterResponseSuccess;
|
} as IPostRegisterResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
|
const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { username, password } = request.body as ILoginRequest;
|
const { username, password } = request.body as IPostLoginRequest;
|
||||||
|
|
||||||
const session = await loginUser({
|
const session = await loginUser({
|
||||||
username: username,
|
username: username,
|
||||||
|
|
@ -41,17 +48,44 @@ const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
reply.status(403);
|
||||||
return {
|
return {
|
||||||
username: username,
|
username: username,
|
||||||
error: API_ERROR.ACCESS_DENIED,
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
} as ILoginResponseError;
|
} as IPostLoginResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reply.setCookie("token", session.cookie, {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "none",
|
||||||
|
secure: true,
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 100,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
ownerId: session.userId,
|
ownerId: session.userId,
|
||||||
token: session.token,
|
} as IPostLoginResponseSuccess;
|
||||||
} as ILoginResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { postRegister, postLogin };
|
const getRefresh = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const cookie = request.cookies["token"];
|
||||||
|
|
||||||
|
const refresh = await refreshSession(cookie);
|
||||||
|
|
||||||
|
if (!refresh) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetRefreshResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: refresh[0].id,
|
||||||
|
ownerId: refresh[0].userId,
|
||||||
|
token: refresh[1],
|
||||||
|
} as IGetRefreshResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { postRegister, postLogin, getRefresh };
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as controller from "./auth.js";
|
||||||
const authRoutes = async (fastify: FastifyInstance) => {
|
const authRoutes = async (fastify: FastifyInstance) => {
|
||||||
fastify.post(`/register`, controller.postRegister);
|
fastify.post(`/register`, controller.postRegister);
|
||||||
fastify.post(`/login`, controller.postLogin);
|
fastify.post(`/login`, controller.postLogin);
|
||||||
|
fastify.get(`/refresh`, controller.getRefresh);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { authRoutes };
|
export { authRoutes };
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,53 @@
|
||||||
import type { API_ERROR } from "../errors.js";
|
import type { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
interface IRegisterRequest {
|
interface IPostRegisterRequest {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRegisterResponseSuccess {
|
interface IPostRegisterResponseSuccess {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
registerDate: number;
|
registerDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRegisterResponseError {
|
interface IPostRegisterResponseError {
|
||||||
error: API_ERROR;
|
error: API_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILoginRequest {
|
interface IPostLoginRequest {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILoginResponseSuccess {
|
interface IPostLoginResponseSuccess {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostLoginResponseError {
|
||||||
|
username: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetRefreshResponseSuccess {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILoginResponseError {
|
interface IGetRefreshResponseError {
|
||||||
username: string;
|
|
||||||
error: API_ERROR;
|
error: API_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type IRegisterRequest,
|
type IPostRegisterRequest,
|
||||||
type IRegisterResponseSuccess,
|
type IPostRegisterResponseSuccess,
|
||||||
type IRegisterResponseError,
|
type IPostRegisterResponseError,
|
||||||
type ILoginRequest,
|
type IPostLoginRequest,
|
||||||
type ILoginResponseSuccess,
|
type IPostLoginResponseSuccess,
|
||||||
type ILoginResponseError,
|
type IPostLoginResponseError,
|
||||||
|
type IGetRefreshResponseError,
|
||||||
|
type IGetRefreshResponseSuccess,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||||
import { getDB } from "../../store/store.js";
|
|
||||||
import { hashPassword } from "../../services/auth/auth.js";
|
|
||||||
import { getUserFromAuth } from "../../services/auth/helpers.js";
|
import { getUserFromAuth } from "../../services/auth/helpers.js";
|
||||||
|
|
||||||
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
|
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
|
||||||
|
|
@ -10,42 +8,6 @@ const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
|
||||||
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
|
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
|
||||||
const authHeader = _request.headers["authorization"];
|
const authHeader = _request.headers["authorization"];
|
||||||
return [{ user: await getUserFromAuth(authHeader) }];
|
return [{ user: await getUserFromAuth(authHeader) }];
|
||||||
|
|
||||||
const owner = await getDB().user.create({
|
|
||||||
data: {
|
|
||||||
username: "TestUser",
|
|
||||||
passwordHash: await hashPassword("29144"),
|
|
||||||
email: "testuser@aslan2142.space",
|
|
||||||
admin: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await getDB().community.create({
|
|
||||||
data: {
|
|
||||||
name: "TestCommunity",
|
|
||||||
ownerId: owner.id,
|
|
||||||
members: {
|
|
||||||
connect: {
|
|
||||||
id: owner.id,
|
|
||||||
},
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
username: "Aslo",
|
|
||||||
passwordHash: await hashPassword("pass"),
|
|
||||||
admin: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: "Aslan",
|
|
||||||
passwordHash: await hashPassword("8556"),
|
|
||||||
email: "aslan@aslan2142.space",
|
|
||||||
admin: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return [{ message: "ok" }];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getPing, getTest };
|
export { getPing, getTest };
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
||||||
const user = await getLoggedUserAuth(authHeader);
|
const user = await getLoggedUserAuth(authHeader);
|
||||||
if (user === API_ERROR.ACCESS_DENIED) {
|
if (user === API_ERROR.ACCESS_DENIED) {
|
||||||
reply.status(404);
|
reply.status(403);
|
||||||
return {
|
return {
|
||||||
error: API_ERROR.ACCESS_DENIED,
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
} as IGetLoggedUserResponseError;
|
} as IGetLoggedUserResponseError;
|
||||||
|
|
|
||||||
14
src/index.ts
14
src/index.ts
|
|
@ -1,6 +1,11 @@
|
||||||
|
import Fastify from "fastify";
|
||||||
|
import cors from "@fastify/cors";
|
||||||
|
import cookie from "@fastify/cookie";
|
||||||
|
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
|
|
||||||
import Fastify from "fastify";
|
import { getCookieSecret } from "./services/auth/helpers.js";
|
||||||
|
|
||||||
import { testRoutes } from "./controllers/test/routes.js";
|
import { testRoutes } from "./controllers/test/routes.js";
|
||||||
import { authRoutes } from "./controllers/auth/routes.js";
|
import { authRoutes } from "./controllers/auth/routes.js";
|
||||||
import { userRoutes } from "./controllers/user/routes.js";
|
import { userRoutes } from "./controllers/user/routes.js";
|
||||||
|
|
@ -14,6 +19,13 @@ const app = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.register(cors, {
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.register(cookie, { secret: getCookieSecret() });
|
||||||
|
|
||||||
app.register(testRoutes);
|
app.register(testRoutes);
|
||||||
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
||||||
app.register(userRoutes, { prefix: "/api/v1/user" });
|
app.register(userRoutes, { prefix: "/api/v1/user" });
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import argon2 from "argon2";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
import type { User, Session } from "../../generated/prisma/client.js";
|
import type { User, Session } from "../../generated/prisma/client.js";
|
||||||
import { getDB } from "../../store/store.js";
|
import { getDB } from "../../store/store.js";
|
||||||
|
import {
|
||||||
|
createSessionCookie,
|
||||||
|
createToken,
|
||||||
|
hashPassword,
|
||||||
|
verifyPassword,
|
||||||
|
} from "./helpers.js";
|
||||||
import type { IUserLogin, IUserRegistration } from "./types.js";
|
import type { IUserLogin, IUserRegistration } from "./types.js";
|
||||||
import { getJwtSecret } from "./helpers.js";
|
|
||||||
|
|
||||||
const registerUser = async (
|
const registerUser = async (
|
||||||
registration: IUserRegistration,
|
registration: IUserRegistration,
|
||||||
|
|
@ -50,11 +52,7 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordCorrect = await argon2.verify(
|
if (!(await verifyPassword(user.passwordHash, login.password))) {
|
||||||
user.passwordHash,
|
|
||||||
login.password,
|
|
||||||
);
|
|
||||||
if (!passwordCorrect) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,23 +67,30 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
|
||||||
|
|
||||||
return await getDB().session.create({
|
return await getDB().session.create({
|
||||||
data: {
|
data: {
|
||||||
token: createToken(user.id),
|
cookie: createSessionCookie(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hashPassword = async (password: string): Promise<string> => {
|
const refreshSession = async (
|
||||||
return await argon2.hash(password, {
|
cookie: string | undefined,
|
||||||
type: argon2.argon2id,
|
): Promise<[Session, string] | null> => {
|
||||||
memoryCost: 2 ** 16,
|
if (!cookie) {
|
||||||
timeCost: 4,
|
return null;
|
||||||
parallelism: 1,
|
}
|
||||||
|
|
||||||
|
const session = await getDB().session.findFirst({
|
||||||
|
where: {
|
||||||
|
cookie: cookie,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [session, createToken(session.id)];
|
||||||
};
|
};
|
||||||
|
|
||||||
const createToken = (userId: string) => {
|
export { registerUser, loginUser, refreshSession };
|
||||||
return jwt.sign({ sub: userId }, getJwtSecret());
|
|
||||||
};
|
|
||||||
|
|
||||||
export { registerUser, loginUser, hashPassword };
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,60 @@
|
||||||
|
import argon2 from "argon2";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import type {
|
import crypto from "crypto";
|
||||||
Community,
|
import type { Community, User } from "../../generated/prisma/client.js";
|
||||||
Session,
|
|
||||||
User,
|
|
||||||
} from "../../generated/prisma/client.js";
|
|
||||||
import { getDB } from "../../store/store.js";
|
import { getDB } from "../../store/store.js";
|
||||||
import type { IOwnerCheck } from "./types.js";
|
import type { AccessTokenPayload, IOwnerCheck } from "./types.js";
|
||||||
import type { PERMISSION } from "./permission.js";
|
import type { PERMISSION } from "./permission.js";
|
||||||
|
|
||||||
const getJwtSecret = () => {
|
const getJwtSecret = (): string => {
|
||||||
return process.env.JWT_SECRET || "";
|
return process.env.JWT_SECRET || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyToken = (token: string): string | jwt.JwtPayload | null => {
|
const getCookieSecret = (): string => {
|
||||||
|
return process.env.COOKIE_SECRET || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSessionCookie = () => {
|
||||||
|
return crypto.randomBytes(32).toString("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createToken = (sessionId: string) => {
|
||||||
|
return jwt.sign({ sessionId: sessionId }, getJwtSecret());
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = (token: string): string | null => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, getJwtSecret());
|
const payload = jwt.verify(token, getJwtSecret()) as AccessTokenPayload;
|
||||||
|
return payload.sessionId;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSessionFromToken = async (token: string): Promise<Session | null> => {
|
const hashPassword = async (password: string): Promise<string> => {
|
||||||
return await getDB().session.findFirst({
|
return await argon2.hash(password, {
|
||||||
where: {
|
type: argon2.argon2id,
|
||||||
token: token,
|
memoryCost: 2 ** 16,
|
||||||
},
|
timeCost: 4,
|
||||||
|
parallelism: 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserFromToken = async (token: string): Promise<User | null> => {
|
const verifyPassword = async (
|
||||||
const session = await getSessionFromToken(token);
|
passwordHash: string,
|
||||||
|
passwordToCheck: string,
|
||||||
|
) => {
|
||||||
|
return await argon2.verify(passwordHash, passwordToCheck);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
|
||||||
return await getDB().user.findFirst({
|
return await getDB().user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: session?.userId ?? "invalid",
|
Session: {
|
||||||
|
some: {
|
||||||
|
id: sessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -43,12 +64,12 @@ const getUserFromAuth = async (
|
||||||
): Promise<User | null> => {
|
): Promise<User | null> => {
|
||||||
const token = authHeader?.replace("Bearer ", "");
|
const token = authHeader?.replace("Bearer ", "");
|
||||||
|
|
||||||
const verified = verifyToken(token ?? "") !== null;
|
const sessionId = verifyToken(token ?? "");
|
||||||
if (!verified || !token) {
|
if (!sessionId || !token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUserFromToken(token);
|
const user = await getUserBySessionId(sessionId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -187,9 +208,13 @@ const isUserInCommunity = async (
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getJwtSecret,
|
getJwtSecret,
|
||||||
|
getCookieSecret,
|
||||||
|
createSessionCookie,
|
||||||
|
createToken,
|
||||||
verifyToken,
|
verifyToken,
|
||||||
getSessionFromToken,
|
hashPassword,
|
||||||
getUserFromToken,
|
verifyPassword,
|
||||||
|
getUserBySessionId,
|
||||||
getUserFromAuth,
|
getUserFromAuth,
|
||||||
isUserOwnerOrAdmin,
|
isUserOwnerOrAdmin,
|
||||||
getUserPermissions,
|
getUserPermissions,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { JwtPayload } from "jsonwebtoken";
|
||||||
import type {
|
import type {
|
||||||
Channel,
|
Channel,
|
||||||
Community,
|
Community,
|
||||||
|
|
@ -6,7 +7,10 @@ import type {
|
||||||
Session,
|
Session,
|
||||||
User,
|
User,
|
||||||
} from "../../generated/prisma/client.js";
|
} from "../../generated/prisma/client.js";
|
||||||
import type { PERMISSION } from "./permission.js";
|
|
||||||
|
interface AccessTokenPayload extends JwtPayload {
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IUserRegistration {
|
interface IUserRegistration {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
@ -28,4 +32,9 @@ interface IOwnerCheck {
|
||||||
role?: Role | null;
|
role?: Role | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type IUserRegistration, type IUserLogin, type IOwnerCheck };
|
export {
|
||||||
|
type AccessTokenPayload,
|
||||||
|
type IUserRegistration,
|
||||||
|
type IUserLogin,
|
||||||
|
type IOwnerCheck,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import { validate } from "uuid";
|
import { validate } from "uuid";
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js";
|
import {
|
||||||
|
apiCookie,
|
||||||
|
apiToken,
|
||||||
|
apiGet,
|
||||||
|
apiPost,
|
||||||
|
apiPatch,
|
||||||
|
apiDelete,
|
||||||
|
} from "./api.js";
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
|
|
||||||
|
|
@ -40,14 +47,25 @@ test("shouldn't be able to login", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can login", async () => {
|
test("can login", async () => {
|
||||||
const response = await apiPost(`auth/login`, {
|
const responseArray = await apiCookie(`auth/login`, {
|
||||||
username: state.username,
|
username: state.username,
|
||||||
password: state.password,
|
password: state.password,
|
||||||
});
|
});
|
||||||
|
const response = responseArray[0];
|
||||||
|
state.cookie = responseArray[1];
|
||||||
|
|
||||||
|
assert.equal(validate(response.id), true);
|
||||||
|
assert.equal(validate(response.ownerId), true);
|
||||||
|
assert.equal(response.ownerId, state.userId);
|
||||||
|
|
||||||
|
state.sessionId = response.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can get access token", async () => {
|
||||||
|
const response = await apiToken(`auth/refresh`, state.cookie);
|
||||||
|
|
||||||
assert.equal(validate(response.id), true);
|
assert.equal(validate(response.id), true);
|
||||||
assert.equal(validate(response.ownerId), true);
|
assert.equal(validate(response.ownerId), true);
|
||||||
assert.equal(response.token.length > 0, true);
|
|
||||||
assert.equal(response.ownerId, state.userId);
|
assert.equal(response.ownerId, state.userId);
|
||||||
|
|
||||||
state.sessionId = response.id;
|
state.sessionId = response.id;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import { validate } from "uuid";
|
import { validate } from "uuid";
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js";
|
import {
|
||||||
|
apiCookie,
|
||||||
|
apiToken,
|
||||||
|
apiGet,
|
||||||
|
apiPost,
|
||||||
|
apiPatch,
|
||||||
|
apiDelete,
|
||||||
|
} from "./api.js";
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
|
|
||||||
|
|
@ -30,19 +37,27 @@ test("can create community", async () => {
|
||||||
});
|
});
|
||||||
state.userId2 = responseRegister2.id;
|
state.userId2 = responseRegister2.id;
|
||||||
|
|
||||||
const responseLogin1 = await apiPost(`auth/login`, {
|
const responseLogin1Array = await apiCookie(`auth/login`, {
|
||||||
username: state.username1,
|
username: state.username1,
|
||||||
password: state.password1,
|
password: state.password1,
|
||||||
});
|
});
|
||||||
|
const responseLogin1 = responseLogin1Array[0];
|
||||||
|
state.cookie1 = responseLogin1Array[1];
|
||||||
state.sessionId1 = responseLogin1.id;
|
state.sessionId1 = responseLogin1.id;
|
||||||
state.token1 = responseLogin1.token;
|
|
||||||
|
|
||||||
const responseLogin2 = await apiPost(`auth/login`, {
|
const responseLogin2Array = await apiCookie(`auth/login`, {
|
||||||
username: state.username2,
|
username: state.username2,
|
||||||
password: state.password2,
|
password: state.password2,
|
||||||
});
|
});
|
||||||
|
const responseLogin2 = responseLogin2Array[0];
|
||||||
|
state.cookie2 = responseLogin2Array[1];
|
||||||
state.sessionId2 = responseLogin2.id;
|
state.sessionId2 = responseLogin2.id;
|
||||||
state.token2 = responseLogin2.token;
|
|
||||||
|
const responseRefresh1 = await apiToken(`auth/refresh`, state.cookie1);
|
||||||
|
state.token1 = responseRefresh1.token;
|
||||||
|
|
||||||
|
const responseRefresh2 = await apiToken(`auth/refresh`, state.cookie2);
|
||||||
|
state.token2 = responseRefresh2.token;
|
||||||
|
|
||||||
const responseCreate = await apiPost(
|
const responseCreate = await apiPost(
|
||||||
`community`,
|
`community`,
|
||||||
|
|
|
||||||
28
tests/api.js
28
tests/api.js
|
|
@ -2,6 +2,32 @@ import config from "../src/config.json" with { type: "json" };
|
||||||
|
|
||||||
const url = `http://localhost:${config.port}/api/v1`;
|
const url = `http://localhost:${config.port}/api/v1`;
|
||||||
|
|
||||||
|
const apiCookie = async (endpoint, request) => {
|
||||||
|
const response = await fetch(`${url}/${endpoint}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseCookie = response.headers.getSetCookie().at(0) ?? "";
|
||||||
|
|
||||||
|
return [await response.json(), responseCookie];
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiToken = async (endpoint, cookie) => {
|
||||||
|
const response = await fetch(`${url}/${endpoint}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: cookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
const apiGet = async (endpoint, token) => {
|
const apiGet = async (endpoint, token) => {
|
||||||
const response = await fetch(`${url}/${endpoint}`, {
|
const response = await fetch(`${url}/${endpoint}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
@ -53,4 +79,4 @@ const apiDelete = async (endpoint, request, token) => {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export { apiGet, apiPost, apiPatch, apiDelete };
|
export { apiCookie, apiToken, apiGet, apiPost, apiPatch, apiDelete };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue