New controllers and services; Auth

This commit is contained in:
Aslan 2025-12-26 19:33:43 +01:00
parent 2fc0f9c404
commit d17f37749d
35 changed files with 1040 additions and 164 deletions

View file

@ -0,0 +1,50 @@
/*
Warnings:
- You are about to drop the column `communityId` on the `User` table. All the data in the column will be lost.
- Added the required column `ownerId` to the `Community` table without a default value. This is not possible if the table is not empty.
- Added the required column `creatorId` to the `Invite` table without a default value. This is not possible if the table is not empty.
- Made the column `communityId` on table `Invite` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Invite" DROP CONSTRAINT "Invite_communityId_fkey";
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_communityId_fkey";
-- AlterTable
ALTER TABLE "Community" ADD COLUMN "ownerId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Invite" ADD COLUMN "creatorId" TEXT NOT NULL,
ALTER COLUMN "communityId" SET NOT NULL;
-- AlterTable
ALTER TABLE "User" DROP COLUMN "communityId";
-- CreateTable
CREATE TABLE "_MembersCommunitiesToUsers" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_MembersCommunitiesToUsers_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_MembersCommunitiesToUsers_B_index" ON "_MembersCommunitiesToUsers"("B");
-- AddForeignKey
ALTER TABLE "Community" ADD CONSTRAINT "Community_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MembersCommunitiesToUsers" ADD CONSTRAINT "_MembersCommunitiesToUsers_A_fkey" FOREIGN KEY ("A") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_MembersCommunitiesToUsers" ADD CONSTRAINT "_MembersCommunitiesToUsers_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "Channel" ADD COLUMN "creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Community" ADD COLUMN "creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Session" ADD COLUMN "creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "permissions" TEXT[];

View file

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "_UsersRoleToUsers" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_UsersRoleToUsers_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_UsersRoleToUsers_B_index" ON "_UsersRoleToUsers"("B");
-- AddForeignKey
ALTER TABLE "_UsersRoleToUsers" ADD CONSTRAINT "_UsersRoleToUsers_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_UsersRoleToUsers" ADD CONSTRAINT "_UsersRoleToUsers_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,31 @@
/*
Warnings:
- You are about to drop the `_UsersRoleToUsers` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_UsersRoleToUsers" DROP CONSTRAINT "_UsersRoleToUsers_A_fkey";
-- DropForeignKey
ALTER TABLE "_UsersRoleToUsers" DROP CONSTRAINT "_UsersRoleToUsers_B_fkey";
-- DropTable
DROP TABLE "_UsersRoleToUsers";
-- CreateTable
CREATE TABLE "_UsersRolesToUsers" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_UsersRolesToUsers_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_UsersRolesToUsers_B_index" ON "_UsersRolesToUsers"("B");
-- AddForeignKey
ALTER TABLE "_UsersRolesToUsers" ADD CONSTRAINT "_UsersRolesToUsers_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_UsersRolesToUsers" ADD CONSTRAINT "_UsersRolesToUsers_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,64 +1,76 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
engineType = "client"
provider = "prisma-client"
output = "../src/generated/prisma"
engineType = "client"
}
datasource db {
provider = "postgresql"
provider = "postgresql"
}
model Community {
id String @id @unique @default(uuid())
name String @unique
description String?
members User[]
Channel Channel[]
Role Role[]
invites Invite[]
id String @id @unique @default(uuid())
name String @unique
description String?
creationDate DateTime @default(now())
User User @relation(name: "OwnerCommunityToUser", fields: [ownerId], references: [id])
ownerId String
members User[] @relation(name: "MembersCommunitiesToUsers")
channels Channel[]
roles Role[]
invites Invite[]
}
model Channel {
id String @id @unique @default(uuid())
name String?
community Community? @relation(fields: [communityId], references: [id])
communityId String?
id String @id @unique @default(uuid())
name String?
community Community? @relation(fields: [communityId], references: [id])
communityId String?
creationDate DateTime @default(now())
}
model Role {
id String @id @unique @default(uuid())
name String?
community Community @relation(fields: [communityId], references: [id])
communityId String
id String @id @unique @default(uuid())
name String?
community Community @relation(fields: [communityId], references: [id])
communityId String
users User[] @relation(name: "UsersRolesToUsers")
permissions String[]
creationDate DateTime @default(now())
}
model User {
id String @id @unique @default(uuid())
username String @unique
email String? @unique
passwordHash String?
description String?
admin Boolean @default(false)
registerDate DateTime @default(now())
lastLogin DateTime?
Community Community? @relation(fields: [communityId], references: [id])
communityId String?
Session Session[]
id String @id @unique @default(uuid())
username String @unique
email String? @unique
passwordHash String?
description String?
admin Boolean @default(false)
registerDate DateTime @default(now())
lastLogin DateTime?
Session Session[]
ownedInvites Invite[]
ownedCommunities Community[] @relation(name: "OwnerCommunityToUser")
communities Community[] @relation(name: "MembersCommunitiesToUsers")
roles Role[] @relation(name: "UsersRolesToUsers")
}
model Session {
id String @id @unique @default(uuid())
owner User @relation(fields: [userId], references: [id])
token String
userId String
id String @id @unique @default(uuid())
owner User @relation(fields: [userId], references: [id])
userId String
token String
creationDate DateTime @default(now())
}
model Invite {
id String @id @unique @default(uuid())
Community Community? @relation(fields: [communityId], references: [id])
communityId String?
totalInvites Int @default(0)
remainingInvites Int @default(0)
creationDate DateTime @default(now())
expirationDate DateTime?
id String @id @unique @default(uuid())
Community Community @relation(fields: [communityId], references: [id])
communityId String
User User @relation(fields: [creatorId], references: [id])
creatorId String
totalInvites Int @default(0)
remainingInvites Int @default(0)
creationDate DateTime @default(now())
expirationDate DateTime?
}

View file

@ -41,7 +41,7 @@ const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => {
if (!session) {
return {
ownerId: "",
username: username,
error: "incorrect credentials",
} as ILoginResponseError;
}

View file

@ -26,7 +26,7 @@ interface ILoginResponseSuccess {
}
interface ILoginResponseError {
ownerId: string;
username: string;
error: string;
}

View file

@ -1,27 +1,38 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IChannelParams,
IChannelResponseError,
IChannelResponseSuccess,
IGetChannelParams,
IGetChannelResponseError,
IGetChannelResponseSuccess,
} from "./types.js";
import { getChannelById } from "../../services/channel/channel.js";
import { getChannelByIdAuth } from "../../services/channel/channel.js";
import { API_ERROR } from "../errors.js";
const getChannel = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IChannelParams;
const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetChannelParams;
const authHeader = request.headers["authorization"];
const channel = await getChannelById(id);
const channel = await getChannelByIdAuth(id, authHeader);
if (!channel) {
reply.status(404);
return {
id: id,
error: "channel does not exist",
} as IChannelResponseError;
error: API_ERROR.NOT_FOUND,
} as IGetChannelResponseError;
}
if (channel === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IGetChannelResponseError;
}
return {
id: channel.id,
name: channel.name,
communityId: channel.communityId,
} as IChannelResponseSuccess;
creationDate: channel.creationDate.getTime(),
} as IGetChannelResponseSuccess;
};
export { getChannel };

View file

@ -1,20 +1,21 @@
interface IChannelParams {
interface IGetChannelParams {
id: string;
}
interface IChannelResponseError {
interface IGetChannelResponseError {
id: string;
error: string;
}
interface IChannelResponseSuccess {
interface IGetChannelResponseSuccess {
id: string;
name: string;
communityId: string;
creationDate: number;
}
export {
type IChannelParams,
type IChannelResponseError,
type IChannelResponseSuccess,
type IGetChannelParams,
type IGetChannelResponseError,
type IGetChannelResponseSuccess,
};

View file

@ -21,6 +21,7 @@ const getCommunity = async (request: FastifyRequest, _reply: FastifyReply) => {
id: community.id,
name: community.name,
description: community.description,
creationDate: community.creationDate.getTime(),
} as ICommunityResponseSuccess;
};

View file

@ -11,6 +11,7 @@ interface ICommunityResponseSuccess {
id: string;
name: string;
description: string;
creationDate: number;
}
export {

View file

@ -0,0 +1,6 @@
enum API_ERROR {
NOT_FOUND = "NOT_FOUND",
ACCESS_DENIED = "ACCESS_DENIED",
}
export { API_ERROR };

View file

@ -0,0 +1,3 @@
export * from "./invite.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -0,0 +1,110 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IDeleteInviteParams,
IDeleteInviteResponseError,
IDeleteInviteResponseSuccess,
IGetInviteParams,
IGetInviteResponseError,
IGetInviteResponseSuccess,
IPostAcceptInviteParams,
IPostAcceptInviteRequest,
IPostAcceptDeleteInviteResponseError,
IPostAcceptDeleteInviteResponseSuccess,
} from "./types.js";
import {
getInviteById,
deleteInviteByIdAuth,
acceptInviteByIdAuth,
hasUnlimitedInvites,
isInviteValid,
} from "../../services/invite/invite.js";
import { API_ERROR } from "../errors.js";
import { getUserById } from "../../services/user/user.js";
const getInvite = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetInviteParams;
const invite = await getInviteById(id);
if (!invite) {
reply.status(404);
return {
id: id,
error: API_ERROR.NOT_FOUND,
} as IGetInviteResponseError;
}
return {
id: invite.id,
communityId: invite.communityId,
valid: isInviteValid(invite),
unlimitedInvites: hasUnlimitedInvites(invite),
hasExpiration: invite.expirationDate != null,
remainingInvites: invite.remainingInvites,
creationDate: invite.creationDate.getTime(),
expirationDate: invite.expirationDate?.getTime() ?? 0,
} as IGetInviteResponseSuccess;
};
const deleteInvite = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IDeleteInviteParams;
const authHeader = request.headers["authorization"];
const invite = await deleteInviteByIdAuth(id, authHeader);
if (!invite) {
reply.status(404);
return {
id: id,
error: API_ERROR.NOT_FOUND,
} as IDeleteInviteResponseError;
}
if (invite === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IDeleteInviteResponseError;
}
return {
id: invite.id,
communityId: invite.communityId,
} as IDeleteInviteResponseSuccess;
};
const postAcceptInvite = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { id } = request.params as IPostAcceptInviteParams;
const { userId } = request.body as IPostAcceptInviteRequest;
const authHeader = request.headers["authorization"];
const community = await acceptInviteByIdAuth(id, authHeader);
const user = await getUserById(id);
if (!community || !user) {
reply.status(404);
return {
id: id,
userId: userId,
error: API_ERROR.NOT_FOUND,
} as IPostAcceptDeleteInviteResponseError;
}
if (community === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
userId: userId,
error: API_ERROR.ACCESS_DENIED,
} as IPostAcceptDeleteInviteResponseError;
}
return {
id: id,
userId: user.id,
userName: user.username,
communityId: community.id,
communityName: community.name,
} as IPostAcceptDeleteInviteResponseSuccess;
};
export { getInvite, deleteInvite, postAcceptInvite };

View file

@ -0,0 +1,10 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./index.js";
const inviteRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getInvite);
fastify.delete(`/:id`, controller.deleteInvite);
fastify.post(`/:id/accept`, controller.postAcceptInvite);
};
export { inviteRoutes };

View file

@ -0,0 +1,70 @@
import type { API_ERROR } from "../errors.js";
interface IGetInviteParams {
id: string;
}
interface IGetInviteResponseError {
id: string;
error: API_ERROR;
}
interface IGetInviteResponseSuccess {
id: string;
communityId: string;
valid: boolean;
unlimitedInvites: boolean;
hasExpiration: boolean;
remainingInvites: number;
creationDate: number;
expirationDate: number;
}
interface IDeleteInviteParams {
id: string;
}
interface IDeleteInviteResponseError {
id: string;
error: API_ERROR;
}
interface IDeleteInviteResponseSuccess {
id: string;
communityId: string;
}
interface IPostAcceptInviteParams {
id: string;
}
interface IPostAcceptInviteRequest {
userId: string;
}
interface IPostAcceptDeleteInviteResponseError {
id: string;
userId: string;
error: API_ERROR;
}
interface IPostAcceptDeleteInviteResponseSuccess {
id: string;
userId: string;
userName: string;
communityId: string;
communityName: string;
}
export {
type IGetInviteParams,
type IGetInviteResponseError,
type IGetInviteResponseSuccess,
type IDeleteInviteParams,
type IDeleteInviteResponseError,
type IDeleteInviteResponseSuccess,
type IPostAcceptInviteParams,
type IPostAcceptInviteRequest,
type IPostAcceptDeleteInviteResponseError,
type IPostAcceptDeleteInviteResponseSuccess,
};

View file

@ -1,27 +1,38 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IRoleParams,
IRoleResponseError,
IRoleResponseSuccess,
IGetRoleParams,
IGetRoleResponseError,
IGetRoleResponseSuccess,
} from "./types.js";
import { getRoleById } from "../../services/role/role.js";
import { getRoleByIdAuth } from "../../services/role/role.js";
import { API_ERROR } from "../errors.js";
const getRole = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as IRoleParams;
const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetRoleParams;
const authHeader = request.headers["authorization"];
const role = await getRoleById(id);
const role = await getRoleByIdAuth(id, authHeader);
if (!role) {
reply.status(404);
return {
id: id,
error: "role does not exist",
} as IRoleResponseError;
error: API_ERROR.NOT_FOUND,
} as IGetRoleResponseError;
}
if (role === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IGetRoleResponseError;
}
return {
id: role.id,
name: role.name,
communityId: role.communityId,
} as IRoleResponseSuccess;
creationDate: role.creationDate.getTime(),
} as IGetRoleResponseSuccess;
};
export { getRole };

View file

@ -1,16 +1,21 @@
interface IRoleParams {
interface IGetRoleParams {
id: string;
}
interface IRoleResponseError {
interface IGetRoleResponseError {
id: string;
error: string;
}
interface IRoleResponseSuccess {
interface IGetRoleResponseSuccess {
id: string;
name: string;
communityId: string;
creationDate: number;
}
export { type IRoleParams, type IRoleResponseError, type IRoleResponseSuccess };
export {
type IGetRoleParams,
type IGetRoleResponseError,
type IGetRoleResponseSuccess,
};

View file

@ -3,6 +3,7 @@ import * as controller from "./session.js";
const sessionRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getSession);
fastify.delete(`/:id`, controller.deleteSession);
};
export { sessionRoutes };

View file

@ -1,26 +1,69 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
ISessionParams,
ISessionResponseError,
ISessionResponseSuccess,
IGetSessionParams,
IGetSessionResponseError,
IGetSessionResponseSuccess,
IDeleteSessionParams,
IDeleteSessionResponseError,
IDeleteSessionResponseSuccess,
} from "./types.js";
import { getSessionById } from "../../services/session/session.js";
import {
deleteSessionByIdAuth,
getSessionByIdAuth,
} from "../../services/session/session.js";
import { API_ERROR } from "../errors.js";
const getSession = async (request: FastifyRequest, _reply: FastifyReply) => {
const { id } = request.params as ISessionParams;
const getSession = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetSessionParams;
const authHeader = request.headers["authorization"];
const session = await getSessionById(id);
const session = await getSessionByIdAuth(id, authHeader);
if (!session) {
reply.status(404);
return {
id: id,
error: "session does not exist",
} as ISessionResponseError;
error: API_ERROR.NOT_FOUND,
} as IGetSessionResponseError;
}
if (session === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IGetSessionResponseError;
}
return {
id: session.id,
userId: session.userId,
} as ISessionResponseSuccess;
creationDate: session.creationDate.getTime(),
} as IGetSessionResponseSuccess;
};
export { getSession };
const deleteSession = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IDeleteSessionParams;
const authHeader = request.headers["authorization"];
const session = await deleteSessionByIdAuth(id, authHeader);
if (!session) {
reply.status(404);
return {
id: id,
error: API_ERROR.NOT_FOUND,
} as IDeleteSessionResponseError;
}
if (session === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IDeleteSessionResponseError;
}
return {
id: session.id,
userId: session.userId,
} as IDeleteSessionResponseSuccess;
};
export { getSession, deleteSession };

View file

@ -1,19 +1,37 @@
interface ISessionParams {
interface IGetSessionParams {
id: string;
}
interface ISessionResponseError {
interface IGetSessionResponseError {
id: string;
error: string;
}
interface ISessionResponseSuccess {
interface IGetSessionResponseSuccess {
id: string;
userId: string;
creationDate: number;
}
interface IDeleteSessionParams {
id: string;
}
interface IDeleteSessionResponseError {
id: string;
error: string;
}
interface IDeleteSessionResponseSuccess {
id: string;
userId: string;
}
export {
type ISessionParams,
type ISessionResponseError,
type ISessionResponseSuccess,
type IGetSessionParams,
type IGetSessionResponseError,
type IGetSessionResponseSuccess,
type IDeleteSessionParams,
type IDeleteSessionResponseError,
type IDeleteSessionResponseSuccess,
};

View file

@ -1,10 +1,50 @@
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";
const getPing = async (_request: FastifyRequest, _reply: FastifyReply) => {
return [{ message: "pong" }];
};
const getTest = async (_request: FastifyRequest, _reply: FastifyReply) => {
const authHeader = _request.headers["authorization"];
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" }];
};

View file

@ -1,59 +0,0 @@
import jwt from "jsonwebtoken";
import type { Session, User } from "./generated/prisma/client.js";
import { getDB } from "./store/store.js";
const getJwtSecret = () => {
return process.env.JWT_SECRET || "";
};
const verifyToken = (token: string): string | jwt.JwtPayload | null => {
try {
return jwt.verify(token, getJwtSecret());
} catch {
return null;
}
};
const getSessionFromToken = async (token: string): Promise<Session | null> => {
return await getDB().session.findFirst({
where: {
token: token,
},
});
};
const getUserFromToken = async (token: string): Promise<User | null> => {
const session = await getSessionFromToken(token);
return await getDB().user.findFirst({
where: {
id: session?.userId ?? "invalid",
},
});
};
const getUserFromAuth = async (
authHeader: string | undefined,
): Promise<User | null> => {
const token = authHeader?.replace("Bearer ", "");
const verified = verifyToken(token ?? "") !== null;
if (!verified || !token) {
return null;
}
const user = await getUserFromToken(token);
if (!user) {
return null;
}
return user;
};
export {
getJwtSecret,
verifyToken,
getSessionFromToken,
getUserFromToken,
getUserFromAuth,
};

View file

@ -8,6 +8,7 @@ import { sessionRoutes } from "./controllers/session/routes.js";
import { communityRoutes } from "./controllers/community/routes.js";
import { channelRoutes } from "./controllers/channel/routes.js";
import { roleRoutes } from "./controllers/role/routes.js";
import { inviteRoutes } from "./controllers/invite/routes.js";
const app = Fastify({
logger: true,
@ -20,6 +21,7 @@ app.register(sessionRoutes, { prefix: "/api/v1/session" });
app.register(communityRoutes, { prefix: "/api/v1/community" });
app.register(channelRoutes, { prefix: "/api/v1/channel" });
app.register(roleRoutes, { prefix: "/api/v1/role" });
app.register(inviteRoutes, { prefix: "/api/v1/invite" });
app.listen({ port: config.port }, (err, address) => {
if (err) throw err;

View file

@ -4,7 +4,7 @@ import jwt from "jsonwebtoken";
import type { User, Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import type { IUserLogin, IUserRegistration } from "./types.js";
import { getJwtSecret } from "../../helpers.js";
import { getJwtSecret } from "./helpers.js";
const registerUser = async (
registration: IUserRegistration,
@ -38,16 +38,27 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
const user = await getDB().user.findUnique({
where: { username: login.username },
});
const passwordCorrect = await argon2.verify(
user?.passwordHash ?? "",
login.password,
);
if (!user || !passwordCorrect) {
if (!user || !user.passwordHash) {
return null;
}
const passwordCorrect = await argon2.verify(
user.passwordHash,
login.password,
);
if (!passwordCorrect) {
return null;
}
await getDB().user.update({
data: {
lastLogin: new Date(),
},
where: {
id: user.id,
},
});
return await getDB().session.create({
data: {
token: createToken(user.id),

View file

@ -0,0 +1,199 @@
import jwt from "jsonwebtoken";
import type {
Community,
Session,
User,
} from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import type { IOwnerCheck } from "./types.js";
import type { PERMISSION } from "./permission.js";
const getJwtSecret = () => {
return process.env.JWT_SECRET || "";
};
const verifyToken = (token: string): string | jwt.JwtPayload | null => {
try {
return jwt.verify(token, getJwtSecret());
} catch {
return null;
}
};
const getSessionFromToken = async (token: string): Promise<Session | null> => {
return await getDB().session.findFirst({
where: {
token: token,
},
});
};
const getUserFromToken = async (token: string): Promise<User | null> => {
const session = await getSessionFromToken(token);
return await getDB().user.findFirst({
where: {
id: session?.userId ?? "invalid",
},
});
};
const getUserFromAuth = async (
authHeader: string | undefined,
): Promise<User | null> => {
const token = authHeader?.replace("Bearer ", "");
const verified = verifyToken(token ?? "") !== null;
if (!verified || !token) {
return null;
}
const user = await getUserFromToken(token);
if (!user) {
return null;
}
return user;
};
const isUserOwnerOrAdmin = async (
user: User | null,
ownerCheck: IOwnerCheck,
): Promise<boolean> => {
if (!user) {
return false;
}
if (user.admin) {
return true;
}
if (ownerCheck.user !== undefined && ownerCheck?.user?.id !== user.id) {
return false;
}
if (
ownerCheck.session !== undefined &&
ownerCheck.session?.userId !== user.id
) {
return false;
}
if (
ownerCheck.community !== undefined &&
ownerCheck.community?.ownerId !== user.id
) {
return false;
}
if (
ownerCheck.invite !== undefined &&
ownerCheck.invite?.creatorId !== user.id
) {
return false;
}
if (ownerCheck.channel !== undefined) {
return false;
}
if (ownerCheck.role !== undefined) {
return false;
}
return true;
};
const getUserPermissions = async (
user: User | null,
community: Community | null,
): Promise<PERMISSION[]> => {
if (!user || !community) {
return [];
}
const roles = await getDB().role.findMany({
where: {
communityId: community.id,
users: {
some: {
id: user.id,
},
},
},
});
if (!roles || roles.length < 1) {
return [];
}
const permissions = new Set<PERMISSION>();
roles.forEach((role) => {
role.permissions.forEach((permission) => {
permissions.add(permission as PERMISSION);
});
});
return [...permissions];
};
const userHasPermissions = async (
user: User | null,
community: Community | null,
requiredPermissions: PERMISSION[],
): Promise<boolean> => {
if (!user || !community) {
return false;
}
const userPermissions = await getUserPermissions(user, community);
return requiredPermissions.every((requiredPermission) =>
userPermissions.includes(requiredPermission),
);
};
const isUserAllowed = async (
user: User | null,
ownerCheck: IOwnerCheck,
community: Community | null,
requiredPermissions: PERMISSION[],
): Promise<boolean> => {
if (await isUserOwnerOrAdmin(user, ownerCheck)) {
return true;
}
if (await userHasPermissions(user, community, requiredPermissions)) {
return true;
}
return false;
};
const isUserInCommunity = async (
user: User | null,
community: Community | null,
): Promise<boolean> => {
if (!user || !community) {
return false;
}
return (
(await getDB().community.findFirst({
where: {
id: community.id,
members: {
some: {
id: user.id,
},
},
},
})) !== null
);
};
export {
getJwtSecret,
verifyToken,
getSessionFromToken,
getUserFromToken,
getUserFromAuth,
isUserOwnerOrAdmin,
getUserPermissions,
userHasPermissions,
isUserAllowed,
isUserInCommunity,
};

View file

@ -0,0 +1,13 @@
enum PERMISSION {
COMMUNITY_MANAGE = "COMMUNITY_MANAGE",
CHANNELS_MANAGE = "CHANNELS_MANAGE",
CHANNELS_READ = "CHANNELS_READ",
ROLES_READ = "ROLES_READ",
INVITES_CREATE = "INVITES_CREATE",
INVITES_DELETE = "INVITES_DELETE",
ROLES_MANAGE = "ROLES_MANAGE",
USERS_KICK = "USERS_KICK",
USERS_BAN = "USERS_BAN",
}
export { PERMISSION };

View file

@ -1,3 +1,13 @@
import type {
Channel,
Community,
Invite,
Role,
Session,
User,
} from "../../generated/prisma/client.js";
import type { PERMISSION } from "./permission.js";
interface IUserRegistration {
username: string;
password: string;
@ -9,4 +19,13 @@ interface IUserLogin {
password: string;
}
export { type IUserRegistration, type IUserLogin };
interface IOwnerCheck {
user?: User | null;
session?: Session | null;
community?: Community | null;
invite?: Invite | null;
channel?: Channel | null;
role?: Role | null;
}
export { type IUserRegistration, type IUserLogin, type IOwnerCheck };

View file

@ -1,5 +1,9 @@
import { API_ERROR } from "../../controllers/errors.js";
import type { Channel } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js";
const getChannelById = async (id: string): Promise<Channel | null> => {
return await getDB().channel.findUnique({
@ -7,4 +11,28 @@ const getChannelById = async (id: string): Promise<Channel | null> => {
});
};
export { getChannelById };
const getChannelByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Channel | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const channel = await getChannelById(id);
const community = await getCommunityById(channel?.communityId ?? "");
if (
!(await isUserAllowed(
authUser,
{
channel: channel,
},
community,
[PERMISSION.CHANNELS_READ],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return channel;
};
export { getChannelById, getChannelByIdAuth };

View file

@ -0,0 +1 @@
export * from "./invite.js";

View file

@ -0,0 +1,128 @@
import type { Community, Invite } from "../../generated/prisma/client.js";
import {
getUserFromAuth,
isUserAllowed,
isUserInCommunity,
isUserOwnerOrAdmin,
userHasPermissions,
} from "../auth/helpers.js";
import { getDB } from "../../store/store.js";
import { getCommunityById } from "../community/community.js";
import { PERMISSION } from "../auth/permission.js";
import { API_ERROR } from "../../controllers/errors.js";
const getInviteById = async (id: string): Promise<Invite | null> => {
return await getDB().invite.findUnique({
where: { id: id },
});
};
const deleteInviteById = async (id: string): Promise<Invite | null> => {
return await getDB().invite.delete({
where: { id: id },
});
};
const deleteInviteByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Invite | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const invite = await deleteInviteById(id);
const community = await getCommunityById(invite?.communityId ?? "");
if (
!(await isUserAllowed(
authUser,
{
invite: invite,
},
community,
[PERMISSION.INVITES_DELETE],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return invite;
};
const acceptInviteById = async (
id: string,
userId: string,
): Promise<Community | null> => {
const invite = await getInviteById(id);
if (!invite) {
return null;
}
await getDB().invite.update({
where: {
id: id,
},
data: {
remainingInvites: invite?.remainingInvites - 1,
},
});
return await getDB().community.update({
where: {
id: invite.communityId,
},
data: {
members: {
connect: {
id: userId,
},
},
},
});
};
const acceptInviteByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Community | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const invite = await getInviteById(id);
const community = await getCommunityById(invite?.communityId ?? "");
if (!authUser || !invite || !community) {
return API_ERROR.ACCESS_DENIED;
}
if (await isUserInCommunity(authUser, community)) {
return API_ERROR.ACCESS_DENIED;
}
return await acceptInviteById(id, authUser.id);
};
const isInviteValid = (invite: Invite): boolean => {
if (!hasUnlimitedInvites(invite) && invite.remainingInvites < 1) {
return false;
}
const currentDate = Date.now();
if (
invite.expirationDate &&
invite.expirationDate.getTime() <= currentDate
) {
return false;
}
return true;
};
const hasUnlimitedInvites = (invite: Invite): boolean => {
return invite.totalInvites === 0;
};
export {
getInviteById,
deleteInviteById,
deleteInviteByIdAuth,
acceptInviteById,
acceptInviteByIdAuth,
isInviteValid,
hasUnlimitedInvites,
};

View file

@ -1,5 +1,9 @@
import { API_ERROR } from "../../controllers/errors.js";
import type { Role } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js";
const getRoleById = async (id: string): Promise<Role | null> => {
return await getDB().role.findUnique({
@ -7,4 +11,28 @@ const getRoleById = async (id: string): Promise<Role | null> => {
});
};
export { getRoleById };
const getRoleByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Role | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const role = await getRoleById(id);
const community = await getCommunityById(role?.communityId ?? "");
if (
!(await isUserAllowed(
authUser,
{
role: role,
},
community,
[PERMISSION.ROLES_READ],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return role;
};
export { getRoleById, getRoleByIdAuth };

View file

@ -1,5 +1,7 @@
import { API_ERROR } from "../../controllers/errors.js";
import type { Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserOwnerOrAdmin } from "../auth/helpers.js";
const getSessionById = async (id: string): Promise<Session | null> => {
return await getDB().session.findUnique({
@ -7,4 +9,51 @@ const getSessionById = async (id: string): Promise<Session | null> => {
});
};
export { getSessionById };
const getSessionByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Session | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const session = await getSessionById(id);
if (
!(await isUserOwnerOrAdmin(authUser, {
session: session,
}))
) {
return API_ERROR.ACCESS_DENIED;
}
return session;
};
const deleteSessionById = async (id: string): Promise<Session | null> => {
return await getDB().session.delete({
where: { id: id },
});
};
const deleteSessionByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<Session | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const session = await deleteSessionById(id);
if (
!(await isUserOwnerOrAdmin(authUser, {
session: session,
}))
) {
return API_ERROR.ACCESS_DENIED;
}
return session;
};
export {
getSessionById,
getSessionByIdAuth,
deleteSessionById,
deleteSessionByIdAuth,
};

View file

@ -1,5 +1,5 @@
import type { User, Session } from "../../generated/prisma/client.js";
import { getUserFromAuth } from "../../helpers.js";
import { getUserFromAuth, isUserOwnerOrAdmin } from "../auth/helpers.js";
import { getDB } from "../../store/store.js";
const getUserById = async (id: string): Promise<User | null> => {
@ -12,8 +12,12 @@ const getUserSessionsById = async (
id: string,
authHeader: string | undefined,
): Promise<Session[] | null> => {
const user = await getUserFromAuth(authHeader);
if (!user || user.id !== id) {
const authUser = await getUserFromAuth(authHeader);
if (
!(await isUserOwnerOrAdmin(authUser, {
user: await getUserById(id),
}))
) {
return null;
}