Version 0.7.0

This commit is contained in:
Aslan 2026-01-20 14:08:51 -05:00
parent 603d969972
commit 8d3b0fa7d3
44 changed files with 611 additions and 41 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "tether", "name": "tether",
"version": "0.6.0", "version": "0.7.0",
"description": "Communication server using the Nexlink protocol", "description": "Communication server using the Nexlink protocol",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Attachment" ADD COLUMN "iv" TEXT;

View file

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "Channel" ADD COLUMN "category" TEXT,
ADD COLUMN "order" INTEGER;
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "color" TEXT,
ADD COLUMN "order" INTEGER;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "showInMembers" BOOLEAN;

View file

@ -0,0 +1,9 @@
-- AlterTable
CREATE SEQUENCE channel_order_seq;
ALTER TABLE "Channel" ALTER COLUMN "order" SET DEFAULT nextval('channel_order_seq');
ALTER SEQUENCE channel_order_seq OWNED BY "Channel"."order";
-- AlterTable
CREATE SEQUENCE role_order_seq;
ALTER TABLE "Role" ALTER COLUMN "order" SET DEFAULT nextval('role_order_seq');
ALTER SEQUENCE role_order_seq OWNED BY "Role"."order";

View file

@ -28,16 +28,21 @@ model Channel {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
name String name String
description String? description String?
category String?
order Int? @default(autoincrement())
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
communityId String communityId String
creationDate DateTime @default(now())
messages Message[] messages Message[]
creationDate DateTime @default(now())
} }
model Role { model Role {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
name String name String
description String? description String?
color String?
order Int? @default(autoincrement())
showInMembers Boolean?
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
communityId String communityId String
users User[] @relation(name: "UsersRolesToUsers") users User[] @relation(name: "UsersRolesToUsers")
@ -115,6 +120,7 @@ model Reaction {
model Attachment { model Attachment {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
iv String?
filename String filename String
mimetype String mimetype String
size BigInt size BigInt

View file

@ -0,0 +1,35 @@
import { type FastifyReply, type FastifyRequest } from "fastify";
import type {
IPostAnnouncementRequest,
IPostAnnouncementError,
} from "./types.js";
import { API_ERROR } from "../errors.js";
import { createAnnouncementAuth } from "../../services/announcement/announcement.js";
const postAnnouncement = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { title, text } = request.body as IPostAnnouncementRequest;
const authHeader = request.headers["authorization"];
const result = await createAnnouncementAuth(
{
title: title,
text: text,
},
authHeader,
);
if (result === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IPostAnnouncementError;
}
reply.status(200);
return;
};
export { postAnnouncement };

View file

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

View file

@ -0,0 +1,8 @@
import { type FastifyInstance } from "fastify";
import * as controller from "./announcement.js";
const announcementRoutes = async (fastify: FastifyInstance) => {
fastify.post(`/`, controller.postAnnouncement);
};
export { announcementRoutes };

View file

@ -0,0 +1,12 @@
import type { API_ERROR } from "../errors.js";
interface IPostAnnouncementRequest {
title: string;
text: string;
}
interface IPostAnnouncementError {
error: API_ERROR;
}
export { type IPostAnnouncementRequest, type IPostAnnouncementError };

View file

@ -6,6 +6,8 @@ const responseFullChannel = (channel: Channel) =>
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,
description: channel.description, description: channel.description,
category: channel.category,
order: channel.order,
communityId: channel.communityId, communityId: channel.communityId,
creationDate: channel.creationDate.getTime(), creationDate: channel.creationDate.getTime(),
}) as IChannel; }) as IChannel;

View file

@ -4,6 +4,8 @@ interface IChannel {
id: string; id: string;
name: string; name: string;
description: string; description: string;
category: string;
order: number;
communityId: string; communityId: string;
creationDate: number; creationDate: number;
} }
@ -39,6 +41,7 @@ interface IPatchChannelParams {
interface IPatchChannelRequest { interface IPatchChannelRequest {
name?: string; name?: string;
description?: string; description?: string;
category?: string;
} }
interface IPatchChannelResponseError { interface IPatchChannelResponseError {

View file

@ -25,6 +25,14 @@ import type {
IGetInvitesParams, IGetInvitesParams,
IGetInvitesResponseError, IGetInvitesResponseError,
IGetInvitesResponseSuccess, IGetInvitesResponseSuccess,
IPatchCommunityChannelOrderParams,
IPatchCommunityChannelOrderRequest,
IPatchCommunityChannelOrderResponseError,
IPatchCommunityChannelOrderResponseSuccess,
IPatchCommunityRoleOrderParams,
IPatchCommunityRoleOrderRequest,
IPatchCommunityRoleOrderResponseError,
IPatchCommunityRoleOrderResponseSuccess,
IPostCreateInviteParams, IPostCreateInviteParams,
IPostCreateInviteRequest, IPostCreateInviteRequest,
IPostCreateInviteResponseError, IPostCreateInviteResponseError,
@ -42,12 +50,18 @@ import {
getCommunityMembersByIdAuth, getCommunityMembersByIdAuth,
getCommunityRolesByIdAuth, getCommunityRolesByIdAuth,
getCommunityInvitesByIdAuth, getCommunityInvitesByIdAuth,
updateCommunityChannelOrderByIdAuth,
updateCommunityRoleOrderByIdAuth,
createInviteAuth, createInviteAuth,
deleteMemberByIdAuth, deleteMemberByIdAuth,
} from "../../services/community/community.js"; } from "../../services/community/community.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
import type { ICreateInvite } from "../../services/community/types.js"; import type { ICreateInvite } from "../../services/community/types.js";
import { responseFullCommunity } from "./helpers.js"; import { responseFullCommunity } from "./helpers.js";
import {
hasUnlimitedInvites,
isInviteValid,
} from "../../services/invite/invite.js";
const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => { const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetCommunityParams; const { id } = request.params as IGetCommunityParams;
@ -203,6 +217,9 @@ const getChannels = async (request: FastifyRequest, reply: FastifyReply) => {
channels: channels.map((channel) => ({ channels: channels.map((channel) => ({
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,
description: channel.description,
category: channel.category,
order: channel.order,
})), })),
} as IGetChannelsResponseSuccess; } as IGetChannelsResponseSuccess;
}; };
@ -234,6 +251,9 @@ const getRoles = async (request: FastifyRequest, reply: FastifyReply) => {
roles: roles.map((role) => ({ roles: roles.map((role) => ({
id: role.id, id: role.id,
name: role.name, name: role.name,
color: role.color,
order: role.order,
showInMembers: role.showInMembers,
})), })),
} as IGetRolesResponseSuccess; } as IGetRolesResponseSuccess;
}; };
@ -264,10 +284,66 @@ const getInvites = async (request: FastifyRequest, reply: FastifyReply) => {
name: community.name, name: community.name,
invites: invites.map((invite) => ({ invites: invites.map((invite) => ({
id: invite.id, id: invite.id,
communityId: invite.communityId,
valid: isInviteValid(invite),
unlimitedInvites: hasUnlimitedInvites(invite),
hasExpiration: invite.expirationDate != null,
totalInvites: invite.totalInvites,
remainingInvites: invite.remainingInvites,
creationDate: invite.creationDate.getTime(),
expirationDate: invite.expirationDate?.getTime() ?? 0,
})), })),
} as IGetInvitesResponseSuccess; } as IGetInvitesResponseSuccess;
}; };
const patchCommunityChannelOrder = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { id } = request.params as IPatchCommunityChannelOrderParams;
const { order } = request.body as IPatchCommunityChannelOrderRequest;
const authHeader = request.headers["authorization"];
const result = await updateCommunityChannelOrderByIdAuth(
id,
order,
authHeader,
);
if (result === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IPatchCommunityChannelOrderResponseError;
}
return { id } as IPatchCommunityChannelOrderResponseSuccess;
};
const patchCommunityRoleOrder = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { id } = request.params as IPatchCommunityRoleOrderParams;
const { order } = request.body as IPatchCommunityRoleOrderRequest;
const authHeader = request.headers["authorization"];
const result = await updateCommunityRoleOrderByIdAuth(
id,
order,
authHeader,
);
if (result === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IPatchCommunityRoleOrderResponseError;
}
return { id } as IPatchCommunityRoleOrderResponseSuccess;
};
const postCreateInvite = async ( const postCreateInvite = async (
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply, reply: FastifyReply,
@ -347,6 +423,8 @@ export {
getChannels, getChannels,
getRoles, getRoles,
getInvites, getInvites,
patchCommunityChannelOrder,
patchCommunityRoleOrder,
postCreateInvite, postCreateInvite,
deleteCommunityMember, deleteCommunityMember,
}; };

View file

@ -10,6 +10,8 @@ const communityRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id/channels`, controller.getChannels); fastify.get(`/:id/channels`, controller.getChannels);
fastify.get(`/:id/roles`, controller.getRoles); fastify.get(`/:id/roles`, controller.getRoles);
fastify.get(`/:id/invites`, controller.getInvites); fastify.get(`/:id/invites`, controller.getInvites);
fastify.patch(`/:id/channels/order`, controller.patchCommunityChannelOrder);
fastify.patch(`/:id/roles/order`, controller.patchCommunityRoleOrder);
fastify.post(`/:id/invite`, controller.postCreateInvite); fastify.post(`/:id/invite`, controller.postCreateInvite);
fastify.delete(`/:id/members/:memberId`, controller.deleteCommunityMember); fastify.delete(`/:id/members/:memberId`, controller.deleteCommunityMember);
}; };

View file

@ -101,6 +101,9 @@ interface IGetChannelsResponseSuccess {
interface IGetChannelsResponseChannel { interface IGetChannelsResponseChannel {
id: string; id: string;
name: string; name: string;
description: string;
category: string;
order: number;
} }
interface IGetRolesParams { interface IGetRolesParams {
@ -121,6 +124,9 @@ interface IGetRolesResponseSuccess {
interface IGetRolesResponseRole { interface IGetRolesResponseRole {
id: string; id: string;
name: string; name: string;
color: string;
order: number;
showInMembers: boolean;
} }
interface IGetInvitesParams { interface IGetInvitesParams {
@ -140,6 +146,48 @@ interface IGetInvitesResponseSuccess {
interface IGetInvitesResponseInvite { interface IGetInvitesResponseInvite {
id: string; id: string;
communityId: string;
valid: boolean;
unlimitedInvites: boolean;
hasExpiration: boolean;
totalInvites: number;
remainingInvites: number;
creationDate: number;
expirationDate: number;
}
interface IPatchCommunityChannelOrderParams {
id: string;
}
interface IPatchCommunityChannelOrderRequest {
order: string[];
}
interface IPatchCommunityChannelOrderResponseError {
id: string;
error: API_ERROR;
}
interface IPatchCommunityChannelOrderResponseSuccess {
id: string;
}
interface IPatchCommunityRoleOrderParams {
id: string;
}
interface IPatchCommunityRoleOrderRequest {
order: string[];
}
interface IPatchCommunityRoleOrderResponseError {
id: string;
error: API_ERROR;
}
interface IPatchCommunityRoleOrderResponseSuccess {
id: string;
} }
interface IPostCreateInviteParams { interface IPostCreateInviteParams {
@ -207,6 +255,14 @@ export {
type IGetInvitesResponseError, type IGetInvitesResponseError,
type IGetInvitesResponseSuccess, type IGetInvitesResponseSuccess,
type IGetInvitesResponseInvite, type IGetInvitesResponseInvite,
type IPatchCommunityChannelOrderParams,
type IPatchCommunityChannelOrderRequest,
type IPatchCommunityChannelOrderResponseError,
type IPatchCommunityChannelOrderResponseSuccess,
type IPatchCommunityRoleOrderParams,
type IPatchCommunityRoleOrderRequest,
type IPatchCommunityRoleOrderResponseError,
type IPatchCommunityRoleOrderResponseSuccess,
type IPostCreateInviteParams, type IPostCreateInviteParams,
type IPostCreateInviteRequest, type IPostCreateInviteRequest,
type IPostCreateInviteResponseError, type IPostCreateInviteResponseError,

View file

@ -216,12 +216,13 @@ const postCreateAttachment = async (
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply, reply: FastifyReply,
) => { ) => {
const { filename, mimetype, size, communityId } = const { iv, filename, mimetype, size, communityId } =
request.body as IPostCreateAttachmentRequest; request.body as IPostCreateAttachmentRequest;
const authHeader = request.headers["authorization"]; const authHeader = request.headers["authorization"];
const attachment = await createAttachmentAuth( const attachment = await createAttachmentAuth(
{ {
iv: iv,
filename: filename, filename: filename,
mimetype: mimetype, mimetype: mimetype,
size: size, size: size,

View file

@ -4,6 +4,7 @@ import type { IAttachment } from "./types.js";
const responseFullAttachment = (attachment: AttachmentWithChunks) => const responseFullAttachment = (attachment: AttachmentWithChunks) =>
({ ({
id: attachment.id, id: attachment.id,
iv: attachment.iv,
filename: attachment.filename, filename: attachment.filename,
mimetype: attachment.mimetype, mimetype: attachment.mimetype,
size: Number(attachment.size), size: Number(attachment.size),

View file

@ -48,6 +48,7 @@ interface IPostUploadCommunityAvatarResponseSuccess {
interface IAttachment { interface IAttachment {
id: string; id: string;
iv: string;
filename: string; filename: string;
mimetype: string; mimetype: string;
size: number; size: number;
@ -69,6 +70,7 @@ interface IGetAttachmentResponseError {
interface IGetAttachmentResponseSuccess extends IAttachment {} interface IGetAttachmentResponseSuccess extends IAttachment {}
interface IPostCreateAttachmentRequest { interface IPostCreateAttachmentRequest {
iv: string;
filename: string; filename: string;
mimetype: string; mimetype: string;
size: number; size: number;

View file

@ -45,6 +45,7 @@ interface IPatchMessageParams {
} }
interface IPatchMessageRequest { interface IPatchMessageRequest {
iv: string;
text: string; text: string;
} }

View file

@ -6,6 +6,9 @@ const responseFullRole = (role: Role) =>
id: role.id, id: role.id,
name: role.name, name: role.name,
description: role.description, description: role.description,
color: role.color,
order: role.order,
showInMembers: role.showInMembers,
communityId: role.communityId, communityId: role.communityId,
permissions: role.permissions, permissions: role.permissions,
creationDate: role.creationDate.getTime(), creationDate: role.creationDate.getTime(),

View file

@ -21,6 +21,7 @@ import type {
IPostUnassignRoleRequest, IPostUnassignRoleRequest,
IPostUnassignRoleResponseError, IPostUnassignRoleResponseError,
IPostUnassignRoleResponseSuccess, IPostUnassignRoleResponseSuccess,
IGetPermissionsResponseSuccess,
} from "./types.js"; } from "./types.js";
import { import {
assignRoleByIdAuth, assignRoleByIdAuth,
@ -32,6 +33,7 @@ import {
} from "../../services/role/role.js"; } from "../../services/role/role.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
import { responseFullRole } from "./helpers.js"; import { responseFullRole } from "./helpers.js";
import { PERMISSION } from "../../services/auth/permission.js";
const getRole = async (request: FastifyRequest, reply: FastifyReply) => { const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetRoleParams; const { id } = request.params as IGetRoleParams;
@ -178,6 +180,15 @@ const postUnassignRole = async (
} as IPostUnassignRoleResponseSuccess; } as IPostUnassignRoleResponseSuccess;
}; };
const getPermissions = async (
_request: FastifyRequest,
_reply: FastifyReply,
) => {
return {
permissions: Object.values(PERMISSION),
} as IGetPermissionsResponseSuccess;
};
export { export {
getRole, getRole,
patchRole, patchRole,
@ -185,4 +196,5 @@ export {
postCreateRole, postCreateRole,
postAssignRole, postAssignRole,
postUnassignRole, postUnassignRole,
getPermissions,
}; };

View file

@ -8,6 +8,7 @@ const roleRoutes = async (fastify: FastifyInstance) => {
fastify.delete(`/:id`, controller.deleteRole); fastify.delete(`/:id`, controller.deleteRole);
fastify.post(`/:id/assign`, controller.postAssignRole); fastify.post(`/:id/assign`, controller.postAssignRole);
fastify.post(`/:id/unassign`, controller.postUnassignRole); fastify.post(`/:id/unassign`, controller.postUnassignRole);
fastify.get(`/permissions`, controller.getPermissions);
}; };
export { roleRoutes }; export { roleRoutes };

View file

@ -5,6 +5,9 @@ interface IRole {
id: string; id: string;
name: string; name: string;
description: string; description: string;
color: string;
order: number;
showInMembers: boolean;
communityId: string; communityId: string;
permissions: string[]; permissions: string[];
creationDate: number; creationDate: number;
@ -42,6 +45,9 @@ interface IPatchRoleParams {
interface IPatchRoleRequest { interface IPatchRoleRequest {
name?: string; name?: string;
description?: string; description?: string;
color?: string;
showInMembers?: boolean;
permissions?: PERMISSION[];
} }
interface IPatchRoleResponseError { interface IPatchRoleResponseError {
@ -105,6 +111,10 @@ interface IPostUnassignRoleResponseSuccess {
userId: string; userId: string;
} }
interface IGetPermissionsResponseSuccess {
permissions: PERMISSION[];
}
export { export {
type IRole, type IRole,
type IGetRoleParams, type IGetRoleParams,
@ -128,4 +138,5 @@ export {
type IPostUnassignRoleRequest, type IPostUnassignRoleRequest,
type IPostUnassignRoleResponseError, type IPostUnassignRoleResponseError,
type IPostUnassignRoleResponseSuccess, type IPostUnassignRoleResponseSuccess,
type IGetPermissionsResponseSuccess,
}; };

View file

@ -9,6 +9,10 @@ const userRoutes = async (fastify: FastifyInstance) => {
fastify.delete(`/:id`, controller.deleteUser); fastify.delete(`/:id`, controller.deleteUser);
fastify.get(`/:id/sessions`, controller.getSessions); fastify.get(`/:id/sessions`, controller.getSessions);
fastify.get(`/:id/communities`, controller.getCommunities); fastify.get(`/:id/communities`, controller.getCommunities);
fastify.get(
`/:id/community/:communityId/roles`,
controller.getCommunityRoles,
);
}; };
export { userRoutes }; export { userRoutes };

View file

@ -120,6 +120,28 @@ interface IGetCommunitiesResponseCommunity {
avatar?: string; avatar?: string;
} }
interface IGetCommunityRolesParams {
id: string;
communityId: string;
}
interface IGetCommunityRolesResponseError {
id: string;
error: API_ERROR;
}
interface IGetCommunityRolesResponseSuccess {
id: string;
communityId: string;
roles: IGetCommunityRolesResponseCommunity[];
}
interface IGetCommunityRolesResponseCommunity {
id: string;
name: string;
description: string;
}
export { export {
type IUser, type IUser,
type IGetLoggedUserResponseError, type IGetLoggedUserResponseError,
@ -145,4 +167,8 @@ export {
type IGetCommunitiesResponseError, type IGetCommunitiesResponseError,
type IGetCommunitiesResponseSuccess, type IGetCommunitiesResponseSuccess,
type IGetCommunitiesResponseCommunity, type IGetCommunitiesResponseCommunity,
type IGetCommunityRolesParams,
type IGetCommunityRolesResponseError,
type IGetCommunityRolesResponseSuccess,
type IGetCommunityRolesResponseCommunity,
}; };

View file

@ -21,6 +21,9 @@ import type {
IGetCommunitiesParams, IGetCommunitiesParams,
IGetCommunitiesResponseError, IGetCommunitiesResponseError,
IGetCommunitiesResponseSuccess, IGetCommunitiesResponseSuccess,
IGetCommunityRolesParams,
IGetCommunityRolesResponseError,
IGetCommunityRolesResponseSuccess,
} from "./types.js"; } from "./types.js";
import { import {
getUserByIdAuth, getUserByIdAuth,
@ -30,6 +33,7 @@ import {
createUserAuth, createUserAuth,
getUserCommunitiesByIdAuth, getUserCommunitiesByIdAuth,
getLoggedUserAuth, getLoggedUserAuth,
getUserCommunityRolesByIdAuth,
} from "../../services/user/user.js"; } from "../../services/user/user.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
import { responseFullUser } from "./helpers.js"; import { responseFullUser } from "./helpers.js";
@ -201,6 +205,44 @@ const getCommunities = async (request: FastifyRequest, reply: FastifyReply) => {
} as IGetCommunitiesResponseSuccess; } as IGetCommunitiesResponseSuccess;
}; };
const getCommunityRoles = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { id, communityId } = request.params as IGetCommunityRolesParams;
const authHeader = request.headers["authorization"];
const roles = await getUserCommunityRolesByIdAuth(
id,
communityId,
authHeader,
);
if (!roles) {
reply.status(404);
return {
id: id,
error: API_ERROR.NOT_FOUND,
} as IGetCommunityRolesResponseError;
}
if (roles === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IGetCommunityRolesResponseError;
}
return {
id: id,
communityId: communityId,
roles: roles.map((role) => ({
id: role.id,
name: role.name,
description: role.description,
})),
} as IGetCommunityRolesResponseSuccess;
};
export { export {
getUserLogged, getUserLogged,
getUser, getUser,
@ -209,4 +251,5 @@ export {
deleteUser, deleteUser,
getSessions, getSessions,
getCommunities, getCommunities,
getCommunityRoles,
}; };

View file

@ -9,6 +9,7 @@ import { config } from "./config.js";
import { getCookieSecret } from "./services/auth/helpers.js"; import { getCookieSecret } from "./services/auth/helpers.js";
import { testRoutes } from "./controllers/test/routes.js"; import { testRoutes } from "./controllers/test/routes.js";
import { announcementRoutes } from "./controllers/announcement/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";
import { sessionRoutes } from "./controllers/session/routes.js"; import { sessionRoutes } from "./controllers/session/routes.js";
@ -45,6 +46,7 @@ app.register(multipart, {
}); });
app.register(testRoutes); app.register(testRoutes);
app.register(announcementRoutes, { prefix: "/api/v1/announcement" });
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" });
app.register(sessionRoutes, { prefix: "/api/v1/session" }); app.register(sessionRoutes, { prefix: "/api/v1/session" });

View file

@ -0,0 +1,30 @@
import { API_ERROR } from "../../controllers/errors.js";
import { getUserFromAuth } from "../auth/helpers.js";
import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToEveryone } from "../websocket/websocket.js";
import type { ICreateAnnouncement } from "./types.js";
const createAnnouncement = async (create: ICreateAnnouncement) => {
sendMessageToEveryone({
type: SocketMessageTypes.ANNOUNCEMENT,
payload: {
title: create.title,
text: create.text,
},
});
};
const createAnnouncementAuth = async (
create: ICreateAnnouncement,
authHeader: string | undefined,
): Promise<void | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
if (!authUser?.admin) {
//return API_ERROR.ACCESS_DENIED;
}
return await createAnnouncement(create);
};
export { createAnnouncement, createAnnouncementAuth };

View file

@ -0,0 +1,2 @@
export * from "./announcement.js";
export * from "./types.js";

View file

@ -0,0 +1,6 @@
interface ICreateAnnouncement {
title: string;
text: string;
}
export { type ICreateAnnouncement };

View file

@ -4,6 +4,7 @@ import { getDB } from "../../store/store.js";
import { import {
createSessionCookie, createSessionCookie,
createToken, createToken,
getRandomBytesBase64,
getRandomBytesHex, getRandomBytesHex,
hashPassword, hashPassword,
verifyPassword, verifyPassword,
@ -77,7 +78,7 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
userId: user.id, userId: user.id,
name: sessionName, name: sessionName,
userAgent: login.userAgent, userAgent: login.userAgent,
storageSecret: `${getRandomBytesHex(32)};${getRandomBytesHex(12)}`, storageSecret: `${getRandomBytesBase64(32)};${getRandomBytesBase64(12)}`,
}, },
}); });
}; };

View file

@ -18,6 +18,10 @@ const getRandomBytesHex = (amount: number) => {
return crypto.randomBytes(amount).toString("hex"); return crypto.randomBytes(amount).toString("hex");
}; };
const getRandomBytesBase64 = (amount: number) => {
return crypto.randomBytes(amount).toString("base64");
};
const createSessionCookie = () => { const createSessionCookie = () => {
return getRandomBytesHex(32); return getRandomBytesHex(32);
}; };
@ -243,6 +247,7 @@ export {
getJwtSecret, getJwtSecret,
getCookieSecret, getCookieSecret,
getRandomBytesHex, getRandomBytesHex,
getRandomBytesBase64,
createSessionCookie, createSessionCookie,
createToken, createToken,
verifyToken, verifyToken,

View file

@ -177,6 +177,7 @@ const getChannelMessagesById = async (
id: string, id: string,
): Promise<FullMessage[] | null> => { ): Promise<FullMessage[] | null> => {
return await getDB().message.findMany({ return await getDB().message.findMany({
take: 50,
include: { include: {
reactions: { reactions: {
select: { select: {

View file

@ -5,6 +5,9 @@ interface ICreateChannel {
interface IUpdateChannel { interface IUpdateChannel {
name?: string; name?: string;
description?: string;
category?: string;
order?: number;
} }
export { type ICreateChannel, type IUpdateChannel }; export { type ICreateChannel, type IUpdateChannel };

View file

@ -7,6 +7,8 @@ import {
isUserOwnerOrAdmin, isUserOwnerOrAdmin,
} from "../auth/helpers.js"; } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js"; import { PERMISSION } from "../auth/permission.js";
import { getChannelById } from "../channel/channel.js";
import { getRoleById } from "../role/role.js";
import { getUserIdsInCommunity } from "../user/user.js"; import { getUserIdsInCommunity } from "../user/user.js";
import { SocketMessageTypes } from "../websocket/types.js"; import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js"; import { sendMessageToUsersWS } from "../websocket/websocket.js";
@ -16,7 +18,6 @@ import type {
ICommunityChannel, ICommunityChannel,
ICommunityMember, ICommunityMember,
ICommunityRole, ICommunityRole,
ICommunityInvite,
ICreateInvite, ICreateInvite,
} from "./types.js"; } from "./types.js";
@ -60,7 +61,7 @@ const updateCommunityById = async (
id: string, id: string,
update: IUpdateCommunity, update: IUpdateCommunity,
): Promise<Community | null> => { ): Promise<Community | null> => {
return await getDB().community.update({ const updatedCommunity = await getDB().community.update({
where: { where: {
id: id, id: id,
}, },
@ -68,6 +69,17 @@ const updateCommunityById = async (
...update, ...update,
}, },
}); });
const userIds = await getUserIdsInCommunity(id);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_COMMUNITY,
payload: {
communityId: id,
},
});
return updatedCommunity;
}; };
const updateCommunityByIdAuth = async ( const updateCommunityByIdAuth = async (
@ -133,6 +145,7 @@ const getCommunityMembersById = async (
id: true, id: true,
username: true, username: true,
nickname: true, nickname: true,
avatar: true,
}, },
}); });
}; };
@ -170,6 +183,9 @@ const getCommunityChannelsById = async (
select: { select: {
id: true, id: true,
name: true, name: true,
description: true,
category: true,
order: true,
}, },
}); });
}; };
@ -205,6 +221,9 @@ const getCommunityRolesById = async (id: string): Promise<ICommunityRole[]> => {
select: { select: {
id: true, id: true,
name: true, name: true,
color: true,
order: true,
showInMembers: true,
}, },
}); });
}; };
@ -232,23 +251,18 @@ const getCommunityRolesByIdAuth = async (
return await getCommunityRolesById(id); return await getCommunityRolesById(id);
}; };
const getCommunityInvitesById = async ( const getCommunityInvitesById = async (id: string): Promise<Invite[]> => {
id: string,
): Promise<ICommunityInvite[]> => {
return await getDB().invite.findMany({ return await getDB().invite.findMany({
where: { where: {
communityId: id, communityId: id,
}, },
select: {
id: true,
},
}); });
}; };
const getCommunityInvitesByIdAuth = async ( const getCommunityInvitesByIdAuth = async (
id: string, id: string,
authHeader: string | undefined, authHeader: string | undefined,
): Promise<ICommunityInvite[] | API_ERROR.ACCESS_DENIED> => { ): Promise<Invite[] | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader); const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(id); const community = await getCommunityById(id);
@ -268,6 +282,118 @@ const getCommunityInvitesByIdAuth = async (
return await getCommunityInvitesById(id); return await getCommunityInvitesById(id);
}; };
const updateCommunityChannelOrderById = async (id: string, order: string[]) => {
for (let i = 0; i < order.length; i++) {
const channelId = order[i];
if (!channelId) {
continue;
}
const channel = await getChannelById(channelId);
if (channel?.communityId !== id) {
continue;
}
await getDB().channel.update({
where: {
id: channelId,
},
data: {
order: i,
},
});
}
const userIds = await getUserIdsInCommunity(id);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_CHANNELS,
payload: {
communityId: id,
},
});
};
const updateCommunityChannelOrderByIdAuth = async (
id: string,
order: string[],
authHeader: string | undefined,
): Promise<void | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(id);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.CHANNELS_MANAGE],
))
) {
return API_ERROR.ACCESS_DENIED;
}
await updateCommunityChannelOrderById(id, order);
};
const updateCommunityRoleOrderById = async (id: string, order: string[]) => {
for (let i = 0; i < order.length; i++) {
const roleId = order[i];
if (!roleId) {
continue;
}
const role = await getRoleById(roleId);
if (role?.communityId !== id) {
continue;
}
await getDB().role.update({
where: {
id: roleId,
},
data: {
order: i,
},
});
}
const userIds = await getUserIdsInCommunity(id);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_ROLES,
payload: {
communityId: id,
},
});
};
const updateCommunityRoleOrderByIdAuth = async (
id: string,
order: string[],
authHeader: string | undefined,
): Promise<void | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(id);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.ROLES_MANAGE],
))
) {
return API_ERROR.ACCESS_DENIED;
}
await updateCommunityRoleOrderById(id, order);
};
const createInvite = async ( const createInvite = async (
id: string, id: string,
creatorId: string, creatorId: string,
@ -377,6 +503,10 @@ export {
getCommunityRolesByIdAuth, getCommunityRolesByIdAuth,
getCommunityInvitesById, getCommunityInvitesById,
getCommunityInvitesByIdAuth, getCommunityInvitesByIdAuth,
updateCommunityChannelOrderById,
updateCommunityChannelOrderByIdAuth,
updateCommunityRoleOrderById,
updateCommunityRoleOrderByIdAuth,
createInvite, createInvite,
createInviteAuth, createInviteAuth,
deleteMemberById, deleteMemberById,

View file

@ -12,21 +12,23 @@ interface ICommunityMember {
id: string; id: string;
username: string; username: string;
nickname?: string | null; nickname?: string | null;
avatar?: string; avatar?: string | null;
} }
interface ICommunityChannel { interface ICommunityChannel {
id: string; id: string;
name: string; name: string;
description: string | null;
category: string | null;
order: number | null;
} }
interface ICommunityRole { interface ICommunityRole {
id: string; id: string;
name: string; name: string;
} color: string | null;
order: number | null;
interface ICommunityInvite { showInMembers?: boolean | null;
id: string;
} }
interface ICreateInvite { interface ICreateInvite {
@ -41,6 +43,5 @@ export {
type ICommunityMember, type ICommunityMember,
type ICommunityChannel, type ICommunityChannel,
type ICommunityRole, type ICommunityRole,
type ICommunityInvite,
type ICreateInvite, type ICreateInvite,
}; };

View file

@ -7,6 +7,7 @@ interface AttachmentWithChunks extends Attachment {
} }
interface ICreateAttachment { interface ICreateAttachment {
iv: string;
filename: string; filename: string;
mimetype: string; mimetype: string;
size: number; size: number;

View file

@ -102,6 +102,10 @@ const acceptInviteByIdAuth = async (
return API_ERROR.ACCESS_DENIED; return API_ERROR.ACCESS_DENIED;
} }
if (!isInviteValid(invite)) {
return API_ERROR.ACCESS_DENIED;
}
if (await isUserInCommunity(authUser, community)) { if (await isUserInCommunity(authUser, community)) {
return API_ERROR.ACCESS_DENIED; return API_ERROR.ACCESS_DENIED;
} }

View file

@ -250,14 +250,6 @@ const deleteMessageById = async (
}, },
}); });
for (const attachmentId of deletedMessage.attachments) {
await getDB().attachment.delete({
where: {
id: attachmentId.id,
},
});
}
const userIds = const userIds =
await getUserIdsInCommunityReadMessagesPermission(communityId); await getUserIdsInCommunityReadMessagesPermission(communityId);

View file

@ -18,6 +18,7 @@ interface ICreateMessage {
} }
interface IUpdateMessage { interface IUpdateMessage {
iv: string;
text: string; text: string;
} }

View file

@ -8,6 +8,10 @@ interface ICreateRole {
interface IUpdateRole { interface IUpdateRole {
name?: string; name?: string;
description?: string;
color?: string;
order?: number;
showInMembers?: boolean;
permissions?: PERMISSION[]; permissions?: PERMISSION[];
} }

View file

@ -2,16 +2,19 @@ import type {
User, User,
Session, Session,
Community, Community,
Role,
} from "../../generated/prisma/client.js"; } from "../../generated/prisma/client.js";
import { import {
getUserFromAuth, getUserFromAuth,
getUserPermissions, getUserPermissions,
isUserAllowed,
isUserOwnerOrAdmin, isUserOwnerOrAdmin,
} from "../auth/helpers.js"; } from "../auth/helpers.js";
import { getDB } from "../../store/store.js"; import { getDB } from "../../store/store.js";
import { API_ERROR } from "../../controllers/errors.js"; import { API_ERROR } from "../../controllers/errors.js";
import type { ICreateUser, IUpdateUser } from "./types.js"; import type { ICreateUser, IUpdateUser } from "./types.js";
import { PERMISSION } from "../auth/permission.js"; import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js";
const communitiesWithReadableUsersCache = new Map<string, Set<string>>(); const communitiesWithReadableUsersCache = new Map<string, Set<string>>();
@ -105,11 +108,7 @@ const getUserByIdAuth = async (
const authUser = await getUserFromAuth(authHeader); const authUser = await getUserFromAuth(authHeader);
const user = await getUserById(id); const user = await getUserById(id);
if ( if (!authUser) {
!(await isUserOwnerOrAdmin(authUser, {
user: user,
}))
) {
return API_ERROR.ACCESS_DENIED; return API_ERROR.ACCESS_DENIED;
} }
@ -254,6 +253,46 @@ const getUserCommunitiesByIdAuth = async (
return communities; return communities;
}; };
const getUserCommunityRolesById = async (
id: string,
communityId: string,
): Promise<Role[] | null> => {
return await getDB().role.findMany({
where: {
communityId: communityId,
users: {
some: {
id: id,
},
},
},
});
};
const getUserCommunityRolesByIdAuth = async (
id: string,
communityId: string,
authHeader: string | undefined,
): Promise<Role[] | null | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(communityId);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.MEMBERS_READ, PERMISSION.ROLES_READ],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return await getUserCommunityRolesById(id, communityId);
};
const getUserIdsInCommunity = async ( const getUserIdsInCommunity = async (
communityId: string, communityId: string,
): Promise<string[]> => { ): Promise<string[]> => {
@ -318,6 +357,8 @@ export {
getUserSessionsByIdAuth, getUserSessionsByIdAuth,
getUserCommunitiesById, getUserCommunitiesById,
getUserCommunitiesByIdAuth, getUserCommunitiesByIdAuth,
getUserCommunityRolesById,
getUserCommunityRolesByIdAuth,
getUserIdsInCommunity, getUserIdsInCommunity,
getUserIdsInCommunityReadMessagesPermission, getUserIdsInCommunityReadMessagesPermission,
getUserIdsWithRole, getUserIdsWithRole,

View file

@ -14,6 +14,7 @@ enum SocketMessageTypes {
ANNOUNCEMENT = "ANNOUNCEMENT", ANNOUNCEMENT = "ANNOUNCEMENT",
SET_MESSAGE = "SET_MESSAGE", SET_MESSAGE = "SET_MESSAGE",
DELETE_MESSAGE = "DELETE_MESSAGE", DELETE_MESSAGE = "DELETE_MESSAGE",
UPDATE_COMMUNITY = "UPDATE_COMMUNITY",
UPDATE_CHANNELS = "UPDATE_CHANNELS", UPDATE_CHANNELS = "UPDATE_CHANNELS",
UPDATE_ROLES = "UPDATE_ROLES", UPDATE_ROLES = "UPDATE_ROLES",
UPDATE_MEMBERS = "UPDATE_MEMBERS", UPDATE_MEMBERS = "UPDATE_MEMBERS",
@ -28,7 +29,7 @@ type SocketMessage =
type: SocketMessageTypes.ANNOUNCEMENT; type: SocketMessageTypes.ANNOUNCEMENT;
payload: { payload: {
title: string; title: string;
description: string; text: string;
}; };
} }
| { | {
@ -51,6 +52,12 @@ type SocketMessage =
communityId: string; communityId: string;
}; };
} }
| {
type: SocketMessageTypes.UPDATE_COMMUNITY;
payload: {
communityId: string;
};
}
| { | {
type: SocketMessageTypes.UPDATE_ROLES; type: SocketMessageTypes.UPDATE_ROLES;
payload: { payload: {

View file

@ -53,6 +53,14 @@ const onMessageWsHandler = (connection: ISocketConnection) => {
}; };
}; };
const sendMessageToEveryone = (message: SocketMessage) => {
userConnections?.forEach((connections) => {
connections?.forEach((connection) => {
connection.socket.send(JSON.stringify(message));
});
});
};
const sendMessageToUserWS = (userId: string, message: SocketMessage) => { const sendMessageToUserWS = (userId: string, message: SocketMessage) => {
const connections = userConnections.get(userId); const connections = userConnections.get(userId);
@ -70,6 +78,7 @@ const sendMessageToUsersWS = (userIds: string[], message: SocketMessage) => {
export { export {
userConnections, userConnections,
handleNewWebSocket, handleNewWebSocket,
sendMessageToEveryone,
sendMessageToUserWS, sendMessageToUserWS,
sendMessageToUsersWS, sendMessageToUsersWS,
}; };