Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
module.exports = {
extends: '@mate-academy/eslint-config',
env: {
jest: true
jest: true,
},
rules: {
'no-proto': 0
'no-proto': 0,
},
plugins: ['jest']
plugins: ['jest'],
};
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ node_modules

# MacOS
.DS_Store

# env files
*.env
.env*

.env
pnpm-lock.yaml
24 changes: 22 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"init": "mate-scripts init",
"start": "node src/index.js",
"dev": "tsx --watch --env-file=.env --inspect=0.0.0.0 src/index.ts",
"lint": "npm run format && mate-scripts lint",
"format": "prettier --ignore-path .prettierignore --write './src/**/*.{js,ts}'",
"test:only": "mate-scripts test",
Expand All @@ -18,13 +19,32 @@
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.1",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.3",
"@types/ws": "^8.18.1",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"prettier": "^3.3.2"
"prettier": "^3.3.2",
"prisma": "^6.7.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"mateAcademy": {
"projectType": "javascript"
"projectType": "typescript"
},
"dependencies": {
"@prisma/client": "6.7.0",
"close-with-grace": "^2.2.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"ws": "^8.18.2",
"zod": "^3.24.4"
}
}
98 changes: 98 additions & 0 deletions prisma/migrations/20250507042010_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "tokens" (
"id" UUID NOT NULL,
"token" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" UUID NOT NULL,

CONSTRAINT "tokens_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "rooms" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"updated_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creator_id" UUID NOT NULL,

CONSTRAINT "rooms_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "members" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"room_id" UUID NOT NULL,
"user_id" UUID NOT NULL,

CONSTRAINT "members_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "messages" (
"id" SERIAL NOT NULL,
"text" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"room_id" UUID NOT NULL,
"author_id" UUID NOT NULL,

CONSTRAINT "messages_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "users_name_key" ON "users"("name");

-- CreateIndex
CREATE INDEX "users_name_idx" ON "users"("name");

-- CreateIndex
CREATE UNIQUE INDEX "tokens_token_key" ON "tokens"("token");

-- CreateIndex
CREATE UNIQUE INDEX "tokens_user_id_key" ON "tokens"("user_id");

-- CreateIndex
CREATE INDEX "tokens_token_idx" ON "tokens"("token");

-- CreateIndex
CREATE UNIQUE INDEX "rooms_name_key" ON "rooms"("name");

-- CreateIndex
CREATE INDEX "rooms_name_idx" ON "rooms"("name");

-- CreateIndex
CREATE INDEX "members_room_id_user_id_idx" ON "members"("room_id", "user_id");

-- CreateIndex
CREATE UNIQUE INDEX "members_room_id_user_id_key" ON "members"("room_id", "user_id");

-- CreateIndex
CREATE INDEX "messages_room_id_author_id_idx" ON "messages"("room_id", "author_id");

-- AddForeignKey
ALTER TABLE "tokens" ADD CONSTRAINT "tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "rooms" ADD CONSTRAINT "rooms_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "members" ADD CONSTRAINT "members_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "members" ADD CONSTRAINT "members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "messages" ADD CONSTRAINT "messages_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "messages" ADD CONSTRAINT "messages_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
80 changes: 80 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DB_URL")
}

model User {
id String @id @default(uuid()) @db.Uuid
name String @unique
createdAt DateTime @default(now()) @map("created_at")

token Token?
messages Message[]
memberships Member[]
createdRooms Room[]

@@map("users")
@@index([name])
}

model Token {
id String @id @default(uuid()) @db.Uuid
token String @unique
createdAt DateTime @default(now()) @map("created_at")

userId String @unique @map("user_id") @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@map("tokens")
@@index([token])
}

model Room {
id String @id @default(uuid()) @db.Uuid
name String @unique
updatedAt DateTime? @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")

messages Message[]
members Member[]

creatorId String @map("creator_id") @db.Uuid
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)

@@map("rooms")
@@index([name])
}

model Member {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")

roomId String @map("room_id") @db.Uuid
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)

userId String @map("user_id") @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([roomId, userId])
@@map("members")
@@index([roomId, userId])
}

model Message {
id Int @id @default(autoincrement())
text String
createdAt DateTime @default(now()) @map("created_at")

roomId String @map("room_id") @db.Uuid
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)

authorId String @map("author_id") @db.Uuid
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)

@@map("messages")
@@index([roomId, authorId])
}
55 changes: 55 additions & 0 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Request, Response } from 'express';

import { AuthData } from '../types/AuthData';
import { authService } from '../services/auth.service';
import { tokenService } from '../services/token.service';

import { NameSchema } from '../schemas/name.schema';
import { ResponseBody } from '../types/ResponseBody';
import { NormalizedUser } from '../types/NormalizedUser';
import { RefreshTokenSchema } from '../schemas/token.schema';

type AuthResponse = Response<
ResponseBody<{ user: NormalizedUser; accessToken: string }>
>;

class AuthController {
register = async (req: Request<{}, {}, NameSchema>, res: AuthResponse) => {
const { name } = req.body;
const authData = await authService.register(name);

await this.sendAuthentication(res, authData);
};

refreshToken = async (
req: Request & { cookies: RefreshTokenSchema },
res: AuthResponse,
) => {
const { refreshToken } = req.cookies;
const authData = await tokenService.refresh(refreshToken);

await this.sendAuthentication(res, authData);
};

private sendAuthentication = async (
res: AuthResponse,
{ refreshToken, normalizedUser: user, ...otherData }: AuthData,
) => {
res.cookie('refreshToken', refreshToken, {
maxAge: 7 * 24 * 60 * 60 * 1000,
httpOnly: true,
sameSite: 'none',
secure: true,
});

res.status(201).json({
message: 'OK',
data: {
user,
...otherData,
},
});
};
}

export const authController = new AuthController();
32 changes: 32 additions & 0 deletions src/controllers/member.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response } from 'express';
import { memberService } from '../services/member.service';

import { ResponseBody } from '../types/ResponseBody';
import { RoomIdSchema } from '../schemas/roomId.schema';
import { NormalizedUser } from '../types/NormalizedUser';

class MemberController {
join = async (
req: Request<RoomIdSchema>,
res: Response<ResponseBody<NormalizedUser>>,
) => {
const { roomId } = req.params;
const { id: userId } = req.user!;
const normalizedRoom = await memberService.join(roomId, userId);

res.status(200).json({
message: 'OK',
data: normalizedRoom,
});
};

leave = async (req: Request<RoomIdSchema>, res: Response<void>) => {
const { roomId } = req.params;
const { id: userId } = req.user!;

await memberService.leave(roomId, userId);
res.sendStatus(204);
};
}

export const memberController = new MemberController();
37 changes: 37 additions & 0 deletions src/controllers/message.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Request, Response } from 'express';
import { messageService } from '../services/message.service';

import { TextSchema } from '../schemas/text.schema';
import { RoomIdSchema } from '../schemas/roomId.schema';

import { ResponseBody } from '../types/ResponseBody';
import { NormalizedUser } from '../types/NormalizedUser';
import { MessagePreview } from '../types/MessagePreview';

class MessageController {
getAll = async (
req: Request<RoomIdSchema>,
res: Response<ResponseBody<MessagePreview[]>>,
) => {
const { roomId } = req.params;
const { id: userId } = req.user!;
const previews = await messageService.getAll(roomId, userId);

res.json({ message: 'OK', data: previews });
};

create = async (
req: Request<RoomIdSchema, {}, TextSchema>,
res: Response<ResponseBody<MessagePreview>>,
) => {
const { text } = req.body;
const { roomId } = req.params;
const { id: authorId } = req.user!;

const preview = await messageService.create(roomId, authorId, text);

res.status(201).json({ message: 'OK', data: preview });
};
}

export const messageController = new MessageController();
Loading