diff --git a/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte b/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte index f6e1e531..80d867ea 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte @@ -1,11 +1,18 @@ @@ -18,3 +25,10 @@ let currentRoute = $derived(page.url.pathname.split("/").pop() || "home");
{@render children()}
+ + diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index 16b9fb2b..becbe871 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -1,97 +1,107 @@ @@ -116,27 +126,7 @@ onDestroy(async () => {
- - { - codeScannedDrawerOpen = true; - }} - /> - (flashlightOn = !flashlightOn)} - /> -
+> {

You're trying to access the following site

+

Platform Name

+

+ {platform ?? "Unable to get name"} +

+
+ +

Website URL

-

- {scannedData?.content} +

+ {hostname ?? scannedData?.content}

@@ -182,15 +179,7 @@ onDestroy(async () => { > Decline - { - codeScannedDrawerOpen = false; - loggedInDrawerOpen = true; - startScan(); - }} - > + Confirm
@@ -221,7 +210,7 @@ onDestroy(async () => {

You're logged in!

-

You're now connected to this service'

+

You're now connected to {platform}

{
- - diff --git a/infrastructure/evault-provisioner/src/index.ts b/infrastructure/evault-provisioner/src/index.ts index ae4cce77..33cf07b6 100644 --- a/infrastructure/evault-provisioner/src/index.ts +++ b/infrastructure/evault-provisioner/src/index.ts @@ -76,6 +76,7 @@ app.post( res: Response, ) => { try { + console.log("provisioning init"); if (!process.env.PUBLIC_REGISTRY_URL) throw new Error("PUBLIC_REGISTRY_URL is not set"); const { registryEntropy, namespace, verificationId } = req.body; @@ -95,6 +96,7 @@ app.post( if (verification.consumed) throw new Error("This verification ID has already been used"); + console.log("jwk"); const jwksResponse = await axios.get( new URL( `/.well-known/jwks.json`, diff --git a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts index 0e68c439..7a6ea18d 100644 --- a/infrastructure/evault-provisioner/src/templates/evault.nomad.ts +++ b/infrastructure/evault-provisioner/src/templates/evault.nomad.ts @@ -1,5 +1,5 @@ import sha256 from "sha256"; -import * as k8s from '@kubernetes/client-node'; +import * as k8s from "@kubernetes/client-node"; import { execSync } from "child_process"; import { json } from "express"; @@ -37,9 +37,10 @@ export function generatePassword(length = 16): string { * @throws {Error} If the service endpoint cannot be determined from the cluster. */ export async function provisionEVault(w3id: string, eVaultId: string) { - const idParts = w3id.split('@'); - w3id = idParts[idParts.length - 1] - const neo4jPassword = sha256(w3id) + console.log("starting to provision"); + const idParts = w3id.split("@"); + w3id = idParts[idParts.length - 1]; + const neo4jPassword = sha256(w3id); const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -50,99 +51,154 @@ export async function provisionEVault(w3id: string, eVaultId: string) { const namespaceName = `evault-${w3id}`; const containerPort = 4000; - const namespace = await coreApi.createNamespace({ body: { metadata: { name: namespaceName } } }); + const namespace = await coreApi.createNamespace({ + body: { metadata: { name: namespaceName } }, + }); const pvcSpec = (name: string) => ({ metadata: { name, namespace: namespaceName }, spec: { - accessModes: ['ReadWriteOnce'], - resources: { requests: { storage: '1Gi' } } - } + accessModes: ["ReadWriteOnce"], + resources: { requests: { storage: "1Gi" } }, + }, + }); + await coreApi.createNamespacedPersistentVolumeClaim({ + namespace: namespaceName, + body: pvcSpec("neo4j-data"), + }); + await coreApi.createNamespacedPersistentVolumeClaim({ + namespace: namespaceName, + body: pvcSpec("evault-store"), }); - await coreApi.createNamespacedPersistentVolumeClaim({ namespace: namespaceName, body: pvcSpec('neo4j-data') }); - await coreApi.createNamespacedPersistentVolumeClaim({ namespace: namespaceName, body: pvcSpec('evault-store') }); await coreApi.createNamespacedPersistentVolumeClaim({ - namespace: namespaceName, body: { - metadata: { name: 'evault-secrets', namespace: namespaceName }, + namespace: namespaceName, + body: { + metadata: { name: "evault-secrets", namespace: namespaceName }, spec: { - accessModes: ['ReadWriteOnce'], + accessModes: ["ReadWriteOnce"], resources: { requests: { - storage: '2Mi' - } - } - } - } + storage: "2Mi", + }, + }, + }, + }, }); - const deployment = { - metadata: { name: 'evault', namespace: namespaceName }, + metadata: { name: "evault", namespace: namespaceName }, spec: { replicas: 1, - selector: { matchLabels: { app: 'evault' } }, + selector: { matchLabels: { app: "evault" } }, template: { - metadata: { labels: { app: 'evault' } }, + metadata: { labels: { app: "evault" } }, spec: { containers: [ { - name: 'neo4j', - image: 'neo4j:5.15', + name: "neo4j", + image: "neo4j:5.15", ports: [{ containerPort: 7687 }], env: [ - { name: 'NEO4J_AUTH', value: `neo4j/${neo4jPassword}` }, - { name: 'dbms.connector.bolt.listen_address', value: '0.0.0.0:7687' } + { + name: "NEO4J_AUTH", + value: `neo4j/${neo4jPassword}`, + }, + { + name: "dbms.connector.bolt.listen_address", + value: "0.0.0.0:7687", + }, + ], + volumeMounts: [ + { name: "neo4j-data", mountPath: "/data" }, ], - volumeMounts: [{ name: 'neo4j-data', mountPath: '/data' }] }, { - name: 'evault', - image: 'merulauvo/evault:latest', + name: "evault", + image: "merulauvo/evault:latest", ports: [{ containerPort }], env: [ - { name: 'NEO4J_URI', value: 'bolt://localhost:7687' }, - { name: 'NEO4J_USER', value: 'neo4j' }, - { name: 'NEO4J_PASSWORD', value: neo4jPassword }, - { name: 'PORT', value: containerPort.toString() }, - { name: 'W3ID', value: w3id }, - { name: "ENCRYPTION_PASSWORD", value: neo4jPassword }, - { name: "SECRETS_STORE_PATH", value: "/secrets" } + { + name: "NEO4J_URI", + value: "bolt://localhost:7687", + }, + { name: "NEO4J_USER", value: "neo4j" }, + { + name: "NEO4J_PASSWORD", + value: neo4jPassword, + }, + { + name: "PORT", + value: containerPort.toString(), + }, + { name: "W3ID", value: w3id }, + { + name: "ENCRYPTION_PASSWORD", + value: neo4jPassword, + }, + { + name: "SECRETS_STORE_PATH", + value: "/secrets", + }, + ], + volumeMounts: [ + { + name: "evault-store", + mountPath: "/evault/data", + }, ], - volumeMounts: [{ name: 'evault-store', mountPath: '/evault/data' }] - } + }, ], volumes: [ - { name: 'neo4j-data', persistentVolumeClaim: { claimName: 'neo4j-data' } }, - { name: 'evault-store', persistentVolumeClaim: { claimName: 'evault-store' } }, - { name: 'evault-secrets', persistentVolumeClaim: { claimName: "evault-secrets" } } - ] - } - } - } + { + name: "neo4j-data", + persistentVolumeClaim: { claimName: "neo4j-data" }, + }, + { + name: "evault-store", + persistentVolumeClaim: { + claimName: "evault-store", + }, + }, + { + name: "evault-secrets", + persistentVolumeClaim: { + claimName: "evault-secrets", + }, + }, + ], + }, + }, + }, }; - await appsApi.createNamespacedDeployment({ body: deployment, namespace: namespaceName }); + await appsApi.createNamespacedDeployment({ + body: deployment, + namespace: namespaceName, + }); await coreApi.createNamespacedService({ namespace: namespaceName, body: { - apiVersion: 'v1', - kind: 'Service', - metadata: { name: 'evault-service' }, + apiVersion: "v1", + kind: "Service", + metadata: { name: "evault-service" }, spec: { - type: 'LoadBalancer', - selector: { app: 'evault' }, + type: "LoadBalancer", + selector: { app: "evault" }, ports: [ { port: 4000, - targetPort: 4000 - } - ] - } - } + targetPort: 4000, + }, + ], + }, + }, }); - const svc = await coreApi.readNamespacedService({ name: 'evault-service', namespace: namespaceName }); + const svc = await coreApi.readNamespacedService({ + name: "evault-service", + namespace: namespaceName, + }); const spec = svc.spec; const status = svc.status; @@ -156,24 +212,26 @@ export async function provisionEVault(w3id: string, eVaultId: string) { // Fallback: NodePort + Node IP (local clusters or bare-metal) const nodePort = spec?.ports?.[0]?.nodePort; - if (!nodePort) throw new Error('No LoadBalancer or NodePort found.'); + if (!nodePort) throw new Error("No LoadBalancer or NodePort found."); // Try getting an external IP from the cluster nodes const nodes = await coreApi.listNode(); - console.log(JSON.stringify(nodes)) const address = nodes?.items[0].status.addresses.find( - (a) => a.type === 'ExternalIP' || a.type === 'InternalIP' + (a) => a.type === "ExternalIP" || a.type === "InternalIP", )?.address; if (address) { - return `http://${address}:${nodePort}`; + const isMinikubeIp = address === "192.168.49.2"; + return `http://${isMinikubeIp ? address : process.env.IP_ADDR.split("http://")[1]}:${nodePort}`; } // Local fallback: use minikube IP if available try { - const minikubeIP = execSync('minikube ip').toString().trim(); - return `http://${minikubeIP}:${nodePort}` + const minikubeIP = execSync("minikube ip").toString().trim(); + return `http://${minikubeIP}:${nodePort}`; } catch (e) { - throw new Error('Unable to determine service IP (no LoadBalancer, Node IP, or Minikube IP)'); + throw new Error( + "Unable to determine service IP (no LoadBalancer, Node IP, or Minikube IP)", + ); } } diff --git a/platforms/blabsy-api/package.json b/platforms/blabsy-api/package.json new file mode 100644 index 00000000..8a1fe477 --- /dev/null +++ b/platforms/blabsy-api/package.json @@ -0,0 +1,41 @@ +{ + "name": "piqtique-api", + "version": "1.0.0", + "description": "Piqtique Social Media Platform API", + "main": "src/index.ts", + "scripts": { + "start": "ts-node src/index.ts", + "dev": "nodemon --exec ts-node src/index.ts", + "build": "tsc", + "typeorm": "typeorm-ts-node-commonjs", + "migration:generate": "npm run typeorm migration:generate -- -d src/database/data-source.ts", + "migration:run": "npm run typeorm migration:run -- -d src/database/data-source.ts", + "migration:revert": "npm run typeorm migration:revert -- -d src/database/data-source.ts" + }, + "dependencies": { + "axios": "^1.6.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "eventsource-polyfill": "^0.9.6", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3", + "reflect-metadata": "^0.2.1", + "typeorm": "^0.3.20", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.11.24", + "@types/pg": "^8.11.2", + "@types/uuid": "^9.0.8", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", + "eslint": "^8.56.0", + "nodemon": "^3.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/platforms/blabsy-api/src/controllers/AuthController.ts b/platforms/blabsy-api/src/controllers/AuthController.ts new file mode 100644 index 00000000..ce886e51 --- /dev/null +++ b/platforms/blabsy-api/src/controllers/AuthController.ts @@ -0,0 +1,81 @@ +import { Request, Response } from "express"; +import { v4 as uuidv4 } from "uuid"; +import { UserService } from "../services/UserService"; +import { EventEmitter } from "events"; +export class AuthController { + private userService: UserService; + private eventEmitter: EventEmitter; + + constructor() { + this.userService = new UserService(); + this.eventEmitter = new EventEmitter(); + } + + sseStream = async (req: Request, res: Response) => { + const { id } = req.params; + + // Set headers for SSE + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + }); + + const handler = (data: any) => { + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + this.eventEmitter.on(id, handler); + + // Handle client disconnect + req.on("close", () => { + this.eventEmitter.off(id, handler); + res.end(); + }); + + req.on("error", (error) => { + console.error("SSE Error:", error); + this.eventEmitter.off(id, handler); + res.end(); + }); + }; + + getOffer = async (req: Request, res: Response) => { + const url = new URL( + "/api/auth", + process.env.PUBLIC_BLABSY_BASE_URL, + ).toString(); + const session = uuidv4(); + const offer = `w3ds://auth?redirect=${url}&session=${session}&platform=blabsy`; + res.json({ uri: offer }); + }; + + login = async (req: Request, res: Response) => { + try { + const { ename, session } = req.body; + + if (!ename) { + return res.status(400).json({ error: "ename is required" }); + } + + const { user, token } = + await this.userService.findOrCreateUser(ename); + + const data = { + user: { + id: user.id, + ename: user.ename, + isVerified: user.isVerified, + isPrivate: user.isPrivate, + }, + token, + }; + this.eventEmitter.emit(session, data); + res.status(200).send(); + } catch (error) { + console.error("Error during login:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/blabsy-api/src/controllers/CommentController.ts b/platforms/blabsy-api/src/controllers/CommentController.ts new file mode 100644 index 00000000..d3e21ed8 --- /dev/null +++ b/platforms/blabsy-api/src/controllers/CommentController.ts @@ -0,0 +1,93 @@ +import { Request, Response } from "express"; +import { CommentService } from "../services/CommentService"; + +export class CommentController { + private commentService: CommentService; + + constructor() { + this.commentService = new CommentService(); + } + + createComment = async (req: Request, res: Response) => { + try { + const { blabId, text } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const reply = await this.commentService.createComment(blabId, userId, text); + res.status(201).json(reply); + } catch (error) { + console.error("Error creating reply:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + getPostComments = async (req: Request, res: Response) => { + try { + const { blabId } = req.params; + const replies = await this.commentService.getPostComments(blabId); + res.json(replies); + } catch (error) { + console.error("Error fetching replies:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + updateComment = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { text } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const reply = await this.commentService.getCommentById(id); + + if (!reply) { + return res.status(404).json({ error: "Reply not found" }); + } + + if (reply.creator.id !== userId) { + return res.status(403).json({ error: "Forbidden" }); + } + + const updatedReply = await this.commentService.updateComment(id, text); + res.json(updatedReply); + } catch (error) { + console.error("Error updating reply:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + deleteComment = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const reply = await this.commentService.getCommentById(id); + + if (!reply) { + return res.status(404).json({ error: "Reply not found" }); + } + + if (reply.creator.id !== userId) { + return res.status(403).json({ error: "Forbidden" }); + } + + await this.commentService.deleteComment(id); + res.status(204).send(); + } catch (error) { + console.error("Error deleting reply:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/controllers/MessageController.ts b/platforms/blabsy-api/src/controllers/MessageController.ts new file mode 100644 index 00000000..1b071183 --- /dev/null +++ b/platforms/blabsy-api/src/controllers/MessageController.ts @@ -0,0 +1,340 @@ +import { Request, Response } from "express"; +import { ChatService } from "../services/ChatService"; +import { verifyToken } from "../utils/jwt"; +import { AppDataSource } from "../database/data-source"; +import { User } from "../database/entities/User"; + +export class MessageController { + private chatService = new ChatService(); + + // Chat Operations + createChat = async (req: Request, res: Response) => { + try { + const { name, participantIds } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + // Ensure the creator is included in participants + const allParticipants = [ + ...new Set([userId, ...(participantIds || [])]), + ]; + const chat = await this.chatService.createChat( + name, + allParticipants, + ); + res.status(201).json(chat); + } catch (error) { + console.error("Error creating chat:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + getChat = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const userId = req.user?.id; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const chat = await this.chatService.getChatById(chatId); + if (!chat) { + return res.status(404).json({ error: "Chat not found" }); + } + + // Verify user is a participant + if (!chat.users.some((user: User) => user.id === userId)) { + return res + .status(403) + .json({ error: "Not a participant in this chat" }); + } + + // Get messages for the chat + const messages = await this.chatService.getChatMessages( + chatId, + userId, + page, + limit, + ); + + res.json({ + ...chat, + messages: messages.messages, + messagesTotal: messages.total, + messagesPage: messages.page, + messagesTotalPages: messages.totalPages, + }); + } catch (error) { + console.error("Error fetching chat:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + getUserChats = async (req: Request, res: Response) => { + try { + const userId = req.user?.id; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const result = await this.chatService.getUserChats( + userId, + page, + limit, + ); + + // Transform the response to include only necessary data + const transformedChats = result.chats.map((chat) => ({ + id: chat.id, + chatName: chat.chatName, + users: chat.users.map((user: User) => ({ + id: user.id, + username: user.username, + displayName: user.displayName, + profilePictureUrl: user.profilePictureUrl, + })), + latestMessage: chat.latestMessage, + updatedAt: chat.updatedAt, + })); + + res.json({ + ...result, + chats: transformedChats, + }); + } catch (error) { + console.error("Error fetching user chats:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + // Chat Participant Operations + addParticipants = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const { participantIds } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const chat = await this.chatService.addParticipants( + chatId, + participantIds, + ); + res.json(chat); + } catch (error) { + console.error("Error adding participants:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + removeParticipant = async (req: Request, res: Response) => { + try { + const { chatId, userId } = req.params; + const currentUserId = req.user?.id; + + if (!currentUserId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const chat = await this.chatService.removeParticipant( + chatId, + userId, + ); + res.json(chat); + } catch (error) { + console.error("Error removing participant:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + // Message Operations (as sub-resources of Chat) + createMessage = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const { text } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + console.log("asdfasd"); + + const message = await this.chatService.sendMessage( + chatId, + userId, + text, + ); + res.status(201).json(message); + } catch (error) { + console.error("Error sending message:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + getMessages = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const userId = req.user?.id; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 20; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const result = await this.chatService.getChatMessages( + chatId, + userId, + page, + limit, + ); + res.json(result); + } catch (error) { + console.error("Error fetching messages:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + markAsRead = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + await this.chatService.markMessagesAsRead(chatId, userId); + res.status(204).send(); + } catch (error) { + console.error("Error marking messages as read:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + deleteMessage = async (req: Request, res: Response) => { + try { + const { chatId, messageId } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + await this.chatService.deleteMessage(messageId, userId); + res.status(204).send(); + } catch (error) { + console.error("Error deleting message:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + getUnreadCount = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const count = await this.chatService.getUnreadMessageCount( + chatId, + userId, + ); + res.json({ count }); + } catch (error) { + console.error("Error getting unread count:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + // SSE route for chat events + getChatEvents = async (req: Request, res: Response) => { + try { + const { chatId } = req.params; + const token = req.query.token; + if (!token) + return res.status(400).json({ error: "token is required" }); + + const decoded = verifyToken(token as string) as { userId: string }; + + if (!decoded?.userId) { + return res.status(401).json({ error: "Invalid token" }); + } + + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOneBy({ id: decoded.userId }); + if (!user) return res.status(401).json({ error: "Invalid token" }); + const userId = user.id; + + // Verify user is a participant + const chat = await this.chatService.getChatById(chatId); + if (!chat) { + return res.status(404).json({ error: "Chat not found" }); + } + + if (!chat.users.some((user: User) => user.id === userId)) { + return res + .status(403) + .json({ error: "Not a participant in this chat" }); + } + + // Set SSE headers + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 2000; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + // Get messages for the chat + const messages = await this.chatService.getChatMessages( + chatId, + userId, + page, + limit, + ); + + // Send initial connection message + res.write(`data: ${JSON.stringify(messages.messages)}\n\n`); + + // Create event listener for this chat + const eventEmitter = this.chatService.getEventEmitter(); + const eventName = `chat:${chatId}`; + + const messageHandler = (data: any) => { + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + // Add event listener + eventEmitter.on(eventName, messageHandler); + + // Handle client disconnect + req.on("close", () => { + eventEmitter.off(eventName, messageHandler); + res.end(); + }); + } catch (error) { + console.error("Error setting up chat events:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/blabsy-api/src/controllers/PostController.ts b/platforms/blabsy-api/src/controllers/PostController.ts new file mode 100644 index 00000000..40f0f592 --- /dev/null +++ b/platforms/blabsy-api/src/controllers/PostController.ts @@ -0,0 +1,66 @@ +import { Request, Response } from "express"; +import { PostService } from "../services/PostService"; + +export class PostController { + private postService: PostService; + + constructor() { + this.postService = new PostService(); + } + + getFeed = async (req: Request, res: Response) => { + try { + const userId = req.user?.id; + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + const feed = await this.postService.getFollowingFeed(userId, page, limit); + res.json(feed); + } catch (error) { + console.error("Error fetching feed:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + createPost = async (req: Request, res: Response) => { + try { + const userId = req.user?.id; + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { content, images, hashtags } = req.body; + const blab = await this.postService.createPost(userId, { + content, + images, + hashtags, + }); + + res.status(201).json(blab); + } catch (error) { + console.error("Error creating blab:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + toggleLike = async (req: Request, res: Response) => { + try { + const { id: blabId } = req.params; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const blab = await this.postService.toggleLike(blabId, userId); + res.json(blab); + } catch (error) { + console.error("Error toggling like:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/blabsy-api/src/controllers/UserController.ts b/platforms/blabsy-api/src/controllers/UserController.ts new file mode 100644 index 00000000..6b6be1fe --- /dev/null +++ b/platforms/blabsy-api/src/controllers/UserController.ts @@ -0,0 +1,96 @@ +import { Request, Response } from "express"; +import { UserService } from "../services/UserService"; + +export class UserController { + private userService: UserService; + + constructor() { + this.userService = new UserService(); + } + + currentUser = async (req: Request, res: Response) => { + res.json(req.user); + }; + + getProfileById = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ error: "User ID is required" }); + } + + const profile = await this.userService.getProfileById(id); + if (!profile) { + return res.status(404).json({ error: "User not found" }); + } + + res.json(profile); + } catch (error) { + console.error("Error fetching user profile:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + search = async (req: Request, res: Response) => { + try { + const { q } = req.query; + + if (!q || typeof q !== "string") { + return res + .status(400) + .json({ error: "Search query is required" }); + } + + const users = await this.userService.searchUsers(q); + res.json(users); + } catch (error) { + console.error("Error searching users:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + follow = async (req: Request, res: Response) => { + try { + const followerId = req.user?.id; + const { id: followingId } = req.params; + + if (!followerId || !followingId) { + return res + .status(400) + .json({ error: "Missing required fields" }); + } + + const updatedUser = await this.userService.followUser( + followerId, + followingId, + ); + res.json(updatedUser); + } catch (error) { + console.error("Error following user:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; + + updateProfile = async (req: Request, res: Response) => { + try { + const userId = req.user?.id; + const { username, profilePictureUrl, displayName } = req.body; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const updatedUser = await this.userService.updateProfile(userId, { + username, + profilePictureUrl, + displayName, + }); + + res.json(updatedUser); + } catch (error) { + console.error("Error updating user profile:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/blabsy-api/src/database/data-source.ts b/platforms/blabsy-api/src/database/data-source.ts new file mode 100644 index 00000000..4ab7d3dc --- /dev/null +++ b/platforms/blabsy-api/src/database/data-source.ts @@ -0,0 +1,22 @@ +import "reflect-metadata"; +import { DataSource } from "typeorm"; +import { config } from "dotenv"; +import { User } from "./entities/User"; +import path from "path"; +import { Chat } from "./entities/Chat"; +import { MessageReadStatus } from "./entities/MessageReadStatus"; +import { Blab } from "./entities/Blab"; +import { Reply } from "./entities/Reply"; +import { Text } from "./entities/Text"; + +config({ path: path.resolve(__dirname, "../../../../.env") }); + +export const AppDataSource = new DataSource({ + type: "postgres", + url: process.env.BLABSY_DATABASE_URL, + synchronize: false, + logging: process.env.NODE_ENV === "development", + entities: [User, Blab, Reply, Text, Chat, MessageReadStatus], + migrations: ["src/database/migrations/*.ts"], + subscribers: [], +}); diff --git a/platforms/blabsy-api/src/database/entities/Blab.ts b/platforms/blabsy-api/src/database/entities/Blab.ts new file mode 100644 index 00000000..c05e0c75 --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/Blab.ts @@ -0,0 +1,41 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, ManyToMany, JoinTable, OneToMany } from "typeorm"; +import { User } from "./User"; +import { Reply } from "./Reply"; + +@Entity("blabs") +export class Blab{ + @PrimaryGeneratedColumn("uuid") + id!: string; + + @ManyToOne(() => User, (user: User) => user.blabs) + author!: User; + + @Column({ type: "text" }) + content!: string; // was content + + @Column("simple-array", { nullable: true }) + images!: string[]; + + @OneToMany(() => Reply, (comment: Reply) => comment.blab) + replies!: Reply[]; + + @ManyToMany(() => User) + @JoinTable({ + name: "blab_likes", + joinColumn: { name: "blab_id", referencedColumnName: "id" }, + inverseJoinColumn: { name: "user_id", referencedColumnName: "id" } + }) + likedBy!: User[]; // was likes + + @Column("simple-array", { nullable: true }) + hashtags!: string[]; // was tags + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @Column({ default: false }) + isArchived!: boolean; // was isDeleted +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/database/entities/Chat.ts b/platforms/blabsy-api/src/database/entities/Chat.ts new file mode 100644 index 00000000..890db645 --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/Chat.ts @@ -0,0 +1,38 @@ +import { + Entity, + CreateDateColumn, + UpdateDateColumn, + PrimaryGeneratedColumn, + Column, + OneToMany, + ManyToMany, + JoinTable, +} from "typeorm"; +import { User } from "./User"; +import { Text } from "./Text"; + +@Entity() +export class Chat { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ nullable: true }) + chatName!: string; + + @OneToMany(() => Text, (e) => e.chat) + texts!: Text[]; + + @ManyToMany(() => User) + @JoinTable({ + name: "chat_participants", + joinColumn: { name: "chat_id", referencedColumnName: "id" }, + inverseJoinColumn: { name: "user_id", referencedColumnName: "id" } + }) + users!: User[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/platforms/blabsy-api/src/database/entities/MessageReadStatus.ts b/platforms/blabsy-api/src/database/entities/MessageReadStatus.ts new file mode 100644 index 00000000..42fea035 --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/MessageReadStatus.ts @@ -0,0 +1,31 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from "typeorm"; +import { Text } from "./Text"; +import { User } from "./User"; + +@Entity("message_read_status") +export class MessageReadStatus { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @ManyToOne(() => Text) + text!: Text; + + @ManyToOne(() => User) + user!: User; + + @Column({ default: false }) + isRead!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/database/entities/Reply.ts b/platforms/blabsy-api/src/database/entities/Reply.ts new file mode 100644 index 00000000..d8557259 --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/Reply.ts @@ -0,0 +1,35 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, ManyToMany, JoinTable } from "typeorm"; +import { User } from "./User"; +import { Blab } from "./Blab"; + +@Entity("replies") +export class Reply { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @ManyToOne(() => User, (user: User) => user.replies) + creator!: User; + + @ManyToOne(() => Blab, (post: Blab) => post.replies) + blab!: Blab; + + @Column("text") + text!: string; + + @ManyToMany(() => User) + @JoinTable({ + name: "reply_likes", + joinColumn: { name: "replyt_id", referencedColumnName: "id" }, + inverseJoinColumn: { name: "user_id", referencedColumnName: "id" } + }) + likedBy!: User[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @Column({ default: false }) + isArchived!: boolean; +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/database/entities/Text.ts b/platforms/blabsy-api/src/database/entities/Text.ts new file mode 100644 index 00000000..31c51bad --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/Text.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToMany, +} from "typeorm"; +import { User } from "./User"; +import { Chat } from "./Chat"; +import { MessageReadStatus } from "./MessageReadStatus"; + +@Entity("texts") +export class Text { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @ManyToOne(() => User) + author!: User; + + @Column({ type: "text" }) + content!: string; + + @ManyToOne(() => Chat, (e) => e.texts) + chat!: Chat; + + @OneToMany(() => MessageReadStatus, (status) => status.text) + readStatuses!: MessageReadStatus[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @Column({ default: false }) + isArchived!: boolean; +} diff --git a/platforms/blabsy-api/src/database/entities/User.ts b/platforms/blabsy-api/src/database/entities/User.ts new file mode 100644 index 00000000..5bc065fd --- /dev/null +++ b/platforms/blabsy-api/src/database/entities/User.ts @@ -0,0 +1,77 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + ManyToMany, + JoinTable, +} from "typeorm"; +import { Blab} from "./Blab"; +import { Reply} from "./Reply"; +import { Chat } from "./Chat"; + +@Entity("users") +export class User { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ nullable: true }) + username!: string; + + @Column({ nullable: true }) + displayName!: string; + + @Column({ nullable: true }) + bio!: string; + + @Column({ nullable: true }) + profilePictureUrl!: string; + + @Column({ nullable: true }) + bannerUrl!: string; + + @Column({ nullable: true }) + ename!: string; + + @Column({ default: false }) + isVerified!: boolean; + + @Column({ default: false }) + isPrivate!: boolean; + + @OneToMany(() => Blab, (post: Blab) => post.author) + blabs!: Blab[]; + + @OneToMany(() => Reply, (reply: Reply) => reply.creator) + replies!: Reply[]; + + @ManyToMany(() => User) + @JoinTable({ + name: "user_followers", + joinColumn: { name: "user_id", referencedColumnName: "id" }, + inverseJoinColumn: { name: "follower_id", referencedColumnName: "id" }, + }) + followers!: User[]; + + @ManyToMany(() => User) + @JoinTable({ + name: "user_following", + joinColumn: { name: "user_id", referencedColumnName: "id" }, + inverseJoinColumn: { name: "following_id", referencedColumnName: "id" }, + }) + following!: User[]; + + @ManyToMany(() => Chat, (chat) => chat.users) + chats!: Chat[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + + @Column({ default: false }) + isArchived!: boolean; +} diff --git a/platforms/blabsy-api/src/database/migrations/1749294377768-migration.ts b/platforms/blabsy-api/src/database/migrations/1749294377768-migration.ts new file mode 100644 index 00000000..974790d4 --- /dev/null +++ b/platforms/blabsy-api/src/database/migrations/1749294377768-migration.ts @@ -0,0 +1,88 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1749294377768 implements MigrationInterface { + name = 'Migration1749294377768' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "replies" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "text" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "creatorId" uuid, "blabId" uuid, CONSTRAINT "PK_08f619ebe431e27e9d206bea132" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "blabs" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "content" text NOT NULL, "images" text, "hashtags" text, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "authorId" uuid, CONSTRAINT "PK_b0d95cd60d167bef0a53b13a83c" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "message_read_status" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "isRead" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "textId" uuid, "userId" uuid, CONSTRAINT "PK_258e8d92b4e212a121dc10a74d3" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "texts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "content" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "authorId" uuid, "chatId" uuid, CONSTRAINT "PK_ce044efbc0a1872f20feca7e19f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "chat" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatName" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_9d0b2ba74336710fd31154738a5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" character varying, "displayName" character varying, "bio" character varying, "profilePictureUrl" character varying, "bannerUrl" character varying, "ename" character varying, "isVerified" boolean NOT NULL DEFAULT false, "isPrivate" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "reply_likes" ("replyt_id" uuid NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_49ea2d0de64487d96abeb821fc3" PRIMARY KEY ("replyt_id", "user_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_60cd31853a064c673a3d3324dc" ON "reply_likes" ("replyt_id") `); + await queryRunner.query(`CREATE INDEX "IDX_ab114098af787728a1d33dd0d2" ON "reply_likes" ("user_id") `); + await queryRunner.query(`CREATE TABLE "blab_likes" ("blab_id" uuid NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_71e1e125898ad7b8f98ba53a90e" PRIMARY KEY ("blab_id", "user_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_8a0152105b2e6a0c279c384dad" ON "blab_likes" ("blab_id") `); + await queryRunner.query(`CREATE INDEX "IDX_6a02fb4b2afef6af04f030a1c5" ON "blab_likes" ("user_id") `); + await queryRunner.query(`CREATE TABLE "chat_participants" ("chat_id" uuid NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_36c99e4a017767179cc49d0ac74" PRIMARY KEY ("chat_id", "user_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_9946d299e9ccfbee23aa40c554" ON "chat_participants" ("chat_id") `); + await queryRunner.query(`CREATE INDEX "IDX_b4129b3e21906ca57b503a1d83" ON "chat_participants" ("user_id") `); + await queryRunner.query(`CREATE TABLE "user_followers" ("user_id" uuid NOT NULL, "follower_id" uuid NOT NULL, CONSTRAINT "PK_d7b47e785d7dbc74b2f22f30045" PRIMARY KEY ("user_id", "follower_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_a59d62cda8101214445e295cdc" ON "user_followers" ("user_id") `); + await queryRunner.query(`CREATE INDEX "IDX_da722d93356ae3119d6be40d98" ON "user_followers" ("follower_id") `); + await queryRunner.query(`CREATE TABLE "user_following" ("user_id" uuid NOT NULL, "following_id" uuid NOT NULL, CONSTRAINT "PK_5d7e9a83ee6f9b806d569068a30" PRIMARY KEY ("user_id", "following_id"))`); + await queryRunner.query(`CREATE INDEX "IDX_a28a2c27629ac06a41720d01c3" ON "user_following" ("user_id") `); + await queryRunner.query(`CREATE INDEX "IDX_94e1183284db3e697031eb7775" ON "user_following" ("following_id") `); + await queryRunner.query(`ALTER TABLE "replies" ADD CONSTRAINT "FK_34408818aba710d6ea7bb40358a" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "replies" ADD CONSTRAINT "FK_3a74da2d3059288882a69be43fa" FOREIGN KEY ("blabId") REFERENCES "blabs"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "blabs" ADD CONSTRAINT "FK_ea5969bad99c59d4f0af17d4692" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "message_read_status" ADD CONSTRAINT "FK_f8aaa4a571e96838b1e15d5ff63" FOREIGN KEY ("textId") REFERENCES "texts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "message_read_status" ADD CONSTRAINT "FK_00956f27e567b20ea63956a94da" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "texts" ADD CONSTRAINT "FK_16d5fc6d4a731bbd3703793a49c" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "texts" ADD CONSTRAINT "FK_fc650300b13333cbe5ae5fac281" FOREIGN KEY ("chatId") REFERENCES "chat"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "reply_likes" ADD CONSTRAINT "FK_60cd31853a064c673a3d3324dc6" FOREIGN KEY ("replyt_id") REFERENCES "replies"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "reply_likes" ADD CONSTRAINT "FK_ab114098af787728a1d33dd0d25" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "blab_likes" ADD CONSTRAINT "FK_8a0152105b2e6a0c279c384dad3" FOREIGN KEY ("blab_id") REFERENCES "blabs"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "blab_likes" ADD CONSTRAINT "FK_6a02fb4b2afef6af04f030a1c59" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "chat_participants" ADD CONSTRAINT "FK_9946d299e9ccfbee23aa40c5545" FOREIGN KEY ("chat_id") REFERENCES "chat"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "chat_participants" ADD CONSTRAINT "FK_b4129b3e21906ca57b503a1d834" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_followers" ADD CONSTRAINT "FK_a59d62cda8101214445e295cdc8" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_followers" ADD CONSTRAINT "FK_da722d93356ae3119d6be40d988" FOREIGN KEY ("follower_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_following" ADD CONSTRAINT "FK_a28a2c27629ac06a41720d01c30" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_following" ADD CONSTRAINT "FK_94e1183284db3e697031eb7775d" FOREIGN KEY ("following_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_following" DROP CONSTRAINT "FK_94e1183284db3e697031eb7775d"`); + await queryRunner.query(`ALTER TABLE "user_following" DROP CONSTRAINT "FK_a28a2c27629ac06a41720d01c30"`); + await queryRunner.query(`ALTER TABLE "user_followers" DROP CONSTRAINT "FK_da722d93356ae3119d6be40d988"`); + await queryRunner.query(`ALTER TABLE "user_followers" DROP CONSTRAINT "FK_a59d62cda8101214445e295cdc8"`); + await queryRunner.query(`ALTER TABLE "chat_participants" DROP CONSTRAINT "FK_b4129b3e21906ca57b503a1d834"`); + await queryRunner.query(`ALTER TABLE "chat_participants" DROP CONSTRAINT "FK_9946d299e9ccfbee23aa40c5545"`); + await queryRunner.query(`ALTER TABLE "blab_likes" DROP CONSTRAINT "FK_6a02fb4b2afef6af04f030a1c59"`); + await queryRunner.query(`ALTER TABLE "blab_likes" DROP CONSTRAINT "FK_8a0152105b2e6a0c279c384dad3"`); + await queryRunner.query(`ALTER TABLE "reply_likes" DROP CONSTRAINT "FK_ab114098af787728a1d33dd0d25"`); + await queryRunner.query(`ALTER TABLE "reply_likes" DROP CONSTRAINT "FK_60cd31853a064c673a3d3324dc6"`); + await queryRunner.query(`ALTER TABLE "texts" DROP CONSTRAINT "FK_fc650300b13333cbe5ae5fac281"`); + await queryRunner.query(`ALTER TABLE "texts" DROP CONSTRAINT "FK_16d5fc6d4a731bbd3703793a49c"`); + await queryRunner.query(`ALTER TABLE "message_read_status" DROP CONSTRAINT "FK_00956f27e567b20ea63956a94da"`); + await queryRunner.query(`ALTER TABLE "message_read_status" DROP CONSTRAINT "FK_f8aaa4a571e96838b1e15d5ff63"`); + await queryRunner.query(`ALTER TABLE "blabs" DROP CONSTRAINT "FK_ea5969bad99c59d4f0af17d4692"`); + await queryRunner.query(`ALTER TABLE "replies" DROP CONSTRAINT "FK_3a74da2d3059288882a69be43fa"`); + await queryRunner.query(`ALTER TABLE "replies" DROP CONSTRAINT "FK_34408818aba710d6ea7bb40358a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_94e1183284db3e697031eb7775"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a28a2c27629ac06a41720d01c3"`); + await queryRunner.query(`DROP TABLE "user_following"`); + await queryRunner.query(`DROP INDEX "public"."IDX_da722d93356ae3119d6be40d98"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a59d62cda8101214445e295cdc"`); + await queryRunner.query(`DROP TABLE "user_followers"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b4129b3e21906ca57b503a1d83"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9946d299e9ccfbee23aa40c554"`); + await queryRunner.query(`DROP TABLE "chat_participants"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6a02fb4b2afef6af04f030a1c5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8a0152105b2e6a0c279c384dad"`); + await queryRunner.query(`DROP TABLE "blab_likes"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ab114098af787728a1d33dd0d2"`); + await queryRunner.query(`DROP INDEX "public"."IDX_60cd31853a064c673a3d3324dc"`); + await queryRunner.query(`DROP TABLE "reply_likes"`); + await queryRunner.query(`DROP TABLE "users"`); + await queryRunner.query(`DROP TABLE "chat"`); + await queryRunner.query(`DROP TABLE "texts"`); + await queryRunner.query(`DROP TABLE "message_read_status"`); + await queryRunner.query(`DROP TABLE "blabs"`); + await queryRunner.query(`DROP TABLE "replies"`); + } + +} diff --git a/platforms/blabsy-api/src/index.ts b/platforms/blabsy-api/src/index.ts new file mode 100644 index 00000000..f083feff --- /dev/null +++ b/platforms/blabsy-api/src/index.ts @@ -0,0 +1,122 @@ +import "reflect-metadata"; +import express from "express"; +import cors from "cors"; +import { config } from "dotenv"; +import { AppDataSource } from "./database/data-source"; +import { PostController } from "./controllers/PostController"; +import path from "path"; +import { AuthController } from "./controllers/AuthController"; +import { CommentController } from "./controllers/CommentController"; +import { MessageController } from "./controllers/MessageController"; +import { authMiddleware, authGuard } from "./middleware/auth"; +import { UserController } from "./controllers/UserController"; + +config({ path: path.resolve(__dirname, "../../../.env") }); + +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use( + cors({ + origin: "*", + methods: ["GET", "POST", "OPTIONS", "PATCH", "DELETE"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + }), +); +app.use(express.json({ limit: "50mb" })); +app.use(express.urlencoded({ limit: "50mb", extended: true })); + +// Initialize database connection +AppDataSource.initialize() + .then(() => { + console.log("Database connection established"); + }) + .catch((error) => { + console.error("Error connecting to database:", error); + process.exit(1); + }); + +// Controllers +const postController = new PostController(); +const authController = new AuthController(); +const commentController = new CommentController(); +const messageController = new MessageController(); +const userController = new UserController(); + +// Public routes (no auth required) +app.get("/api/auth/offer", authController.getOffer); +app.post("/api/auth", authController.login); +app.get("/api/auth/sessions/:id", authController.sseStream); +app.get("/api/chats/:chatId/events", messageController.getChatEvents); + +// Protected routes (auth required) +app.use(authMiddleware); // Apply auth middleware to all routes below + +// Blab routes +app.get("/api/blabs/feed", authGuard, postController.getFeed); +app.post("/api/blabs", authGuard, postController.createPost); +app.post("/api/blabs/:id/like", authGuard, postController.toggleLike); + +// Reply routes +app.post("/api/replies", authGuard, commentController.createComment); +app.get( + "/api/blabs/:blabId/replies", + authGuard, + commentController.getPostComments, +); +app.put("/api/replies/:id", authGuard, commentController.updateComment); +app.delete("/api/replies/:id", authGuard, commentController.deleteComment); + +// Chat routes +app.post("/api/chats", authGuard, messageController.createChat); +app.get("/api/chats", authGuard, messageController.getUserChats); +app.get("/api/chats/:chatId", authGuard, messageController.getChat); + +// Chat participant routes +app.post( + "/api/chats/:chatId/users", + authGuard, + messageController.addParticipants, +); +app.delete( + "/api/chats/:chatId/users/:userId", + authGuard, + messageController.removeParticipant, +); + +// Chat message routes +app.post( + "/api/chats/:chatId/texts", + authGuard, + messageController.createMessage, +); +app.get("/api/chats/:chatId/texts", authGuard, messageController.getMessages); +app.delete( + "/api/chats/:chatId/texts/:textId", + authGuard, + messageController.deleteMessage, +); +app.post( + "/api/chats/:chatId/texts/read", + authGuard, + messageController.markAsRead, +); +app.get( + "/api/chats/:chatId/texts/unread", + authGuard, + messageController.getUnreadCount, +); + +// User routes +app.get("/api/users", userController.currentUser); +app.get("/api/users/search", userController.search); +app.post("/api/users/:id/follow", authGuard, userController.follow); +app.get("/api/users/:id", authGuard, userController.getProfileById); +app.patch("/api/users", authGuard, userController.updateProfile); + +// Start server +app.listen(port, () => { + console.log(`Server running on port ${port}`); +}); diff --git a/platforms/blabsy-api/src/middleware/auth.ts b/platforms/blabsy-api/src/middleware/auth.ts new file mode 100644 index 00000000..fdace1d2 --- /dev/null +++ b/platforms/blabsy-api/src/middleware/auth.ts @@ -0,0 +1,46 @@ +import { Request, Response, NextFunction } from "express"; +import { AppDataSource } from "../database/data-source"; +import { User } from "../database/entities/User"; +import { verifyToken } from "../utils/jwt"; + +export const authMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith("Bearer ")) { + return res.status(401).json({ error: "No token provided" }); + } + + const token = authHeader.split(" ")[1]; + const decoded = verifyToken(token) as { userId: string }; + + if (!decoded?.userId) { + return res.status(401).json({ error: "Invalid token" }); + } + + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOneBy({ id: decoded.userId }); + + if (!user) { + return res.status(401).json({ error: "User not found" }); + } + + req.user = user; + console.log("user", user.ename); + next(); + } catch (error) { + console.error("Auth middleware error:", error); + res.status(401).json({ error: "Invalid token" }); + } +}; + +export const authGuard = (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); +}; + diff --git a/platforms/blabsy-api/src/services/ChatService.ts b/platforms/blabsy-api/src/services/ChatService.ts new file mode 100644 index 00000000..8839e20b --- /dev/null +++ b/platforms/blabsy-api/src/services/ChatService.ts @@ -0,0 +1,303 @@ +import { AppDataSource } from "../database/data-source"; +import { Chat } from "../database/entities/Chat"; +import { User } from "../database/entities/User"; +import { MessageReadStatus } from "../database/entities/MessageReadStatus"; +import { In } from "typeorm"; +import { EventEmitter } from "events"; +import { Text } from "../database/entities/Text"; + +export class ChatService { + private chatRepository = AppDataSource.getRepository(Chat); + private textRepository = AppDataSource.getRepository(Text); + private userRepository = AppDataSource.getRepository(User); + private messageReadStatusRepository = AppDataSource.getRepository(MessageReadStatus); + private eventEmitter = new EventEmitter(); + + // Event emitter getter + getEventEmitter(): EventEmitter { + return this.eventEmitter; + } + + // Chat CRUD Operations + async createChat( + name?: string, + participantIds: string[] = [], + ): Promise { + const participants = await this.userRepository.findBy({ + id: In(participantIds), + }); + if (participants.length !== participantIds.length) { + throw new Error("One or more participants not found"); + } + + const chat = this.chatRepository.create({ + chatName: name || undefined, + users: participants, + }); + return await this.chatRepository.save(chat); + } + + async getChatById(id: string): Promise { + return await this.chatRepository.findOne({ + where: { id }, + relations: [ + "texts", + "texts.author", + "texts.readStatuses", + "users", + ], + }); + } + + async updateChat(id: string, name: string): Promise { + const chat = await this.getChatById(id); + if (!chat) { + throw new Error("Chat not found"); + } + chat.chatName = name; + return await this.chatRepository.save(chat); + } + + async deleteChat(id: string): Promise { + const chat = await this.getChatById(id); + if (!chat) { + throw new Error("Chat not found"); + } + await this.chatRepository.softDelete(id); + } + + // Participant Operations + async addParticipants( + chatId: string, + participantIds: string[], + ): Promise { + const chat = await this.getChatById(chatId); + if (!chat) { + throw new Error("Chat not found"); + } + + const newParticipants = await this.userRepository.findBy({ + id: In(participantIds), + }); + if (newParticipants.length !== participantIds.length) { + throw new Error("One or more participants not found"); + } + + chat.users = [...chat.users, ...newParticipants]; + return await this.chatRepository.save(chat); + } + + async removeParticipant(chatId: string, userId: string): Promise { + const chat = await this.getChatById(chatId); + if (!chat) { + throw new Error("Chat not found"); + } + + chat.users = chat.users.filter((p) => p.id !== userId); + return await this.chatRepository.save(chat); + } + + // Message Operations + async sendMessage( + chatId: string, + senderId: string, + text: string, + ): Promise { + const chat = await this.getChatById(chatId); + if (!chat) { + throw new Error("Chat not found"); + } + + const sender = await this.userRepository.findOneBy({ id: senderId }); + if (!sender) { + throw new Error("Sender not found"); + } + + // Verify sender is a participant + if (!chat.users.some((p) => p.id === senderId)) { + throw new Error("Sender is not a participant in this chat"); + } + + const message = this.textRepository.create({ + content: text, + author: sender, + chat, + }); + + const savedMessage = await this.textRepository.save(message); + + // Create read status entries for all participants except sender + const readStatuses = chat.users + .filter((p) => p.id !== senderId) + .map((user) => + this.messageReadStatusRepository.create({ + text: savedMessage, + user, + isRead: false, + }), + ); + + await this.messageReadStatusRepository.save(readStatuses); + + // Emit new message event + this.eventEmitter.emit(`chat:${chatId}`, [savedMessage]); + + return savedMessage; + } + + async getChatMessages( + chatId: string, + userId: string, + page: number = 1, + limit: number = 20, + ): Promise<{ + messages: Text[]; + total: number; + page: number; + totalPages: number; + }> { + const [messages, total] = await this.textRepository.findAndCount({ + where: { chat: { id: chatId } }, + relations: ["author", "readStatuses", "readStatuses.user"], + order: { createdAt: "ASC" }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + messages, + total, + page, + totalPages: Math.ceil(total / limit), + }; + } + + async markMessagesAsRead(chatId: string, userId: string): Promise { + const chat = await this.getChatById(chatId); + if (!chat) { + throw new Error("Chat not found"); + } + + // Verify user is a participant + if (!chat.users.some((p) => p.id === userId)) { + throw new Error("User is not a participant in this chat"); + } + + // First get all message IDs for this chat that were sent by other users + const messageIds = await this.textRepository + .createQueryBuilder("text") + .select("text.id") + .where("text.chat.id = :chatId", { chatId }) + .andWhere("text.author.id != :userId", { userId }) // Only messages not sent by the user + .getMany(); + + if (messageIds.length === 0) { + return; // No messages to mark as read + } + + // Then update the read status for these messages + await this.messageReadStatusRepository + .createQueryBuilder() + .update(MessageReadStatus) + .set({ isRead: true }) + .where("text.id IN (:...messageIds)", { messageIds: messageIds.map(m => m.id) }) + .andWhere("user.id = :userId", { userId }) + .andWhere("isRead = :isRead", { isRead: false }) + .execute(); + } + + async deleteMessage(messageId: string, userId: string): Promise { + const message = await this.textRepository.findOne({ + where: { id: messageId }, + relations: ["author"], + }); + + if (!message) { + throw new Error("Message not found"); + } + + if (message.author.id !== userId) { + throw new Error("Unauthorized to delete this message"); + } + + await this.textRepository.softDelete(messageId); + } + + // Additional Utility Methods + async getUserChats( + userId: string, + page: number = 1, + limit: number = 10, + ): Promise<{ + chats: (Chat & { latestMessage?: { content: string; isRead: boolean } })[]; + total: number; + page: number; + totalPages: number; + }> { + // First, get the chat IDs that the user is part of + const [chatIds, total] = await this.chatRepository + .createQueryBuilder("chat") + .select(["chat.id", "chat.updatedAt"]) + .innerJoin("chat.users", "users") + .where("users.id = :userId", { userId }) + .orderBy("chat.updatedAt", "DESC") + .skip((page - 1) * limit) + .take(limit) + .getManyAndCount(); + + // Then, load the full chat data with all relations + const chats = await this.chatRepository.find({ + where: { id: In(chatIds.map((chat) => chat.id)) }, + relations: [ + "users", + "texts", + "texts.author", + "texts.readStatuses", + "texts.readStatuses.user", + ], + order: { updatedAt: "DESC" }, + }); + + // For each chat, get the latest message and its read status + const chatsWithLatestMessage = await Promise.all( + chats.map(async (chat) => { + const latestMessage = chat.texts[chat.texts.length - 1]; + if (!latestMessage) { + return { ...chat, latestMessage: undefined }; + } + + const readStatus = latestMessage.readStatuses.find( + (status) => status.user.id === userId + ); + + return { + ...chat, + latestMessage: { + content: latestMessage.content, + isRead: readStatus?.isRead ?? false, + }, + }; + }) + ); + + return { + chats: chatsWithLatestMessage, + total, + page, + totalPages: Math.ceil(total / limit), + }; + } + + async getUnreadMessageCount( + chatId: string, + userId: string, + ): Promise { + return await this.messageReadStatusRepository.count({ + where: { + text: { chat: { id: chatId } }, + user: { id: userId }, + isRead: false, + }, + }); + } +} diff --git a/platforms/blabsy-api/src/services/CommentService.ts b/platforms/blabsy-api/src/services/CommentService.ts new file mode 100644 index 00000000..935db20b --- /dev/null +++ b/platforms/blabsy-api/src/services/CommentService.ts @@ -0,0 +1,57 @@ +import { AppDataSource } from "../database/data-source"; +import { Reply } from "../database/entities/Reply"; +import { Blab } from "../database/entities/Blab"; + +export class CommentService { + private replyRepository = AppDataSource.getRepository(Reply); + private blabRepository = AppDataSource.getRepository(Blab); + + async createComment(blabId: string, authorId: string, text: string): Promise { + const blab = await this.blabRepository.findOneBy({ id: blabId }); + if (!blab) { + throw new Error('Blab not found'); + } + + const reply = this.replyRepository.create({ + text, + creator: { id: authorId }, + blab: { id: blabId } + }); + + return await this.replyRepository.save(reply); + } + + async getPostComments(blabId: string): Promise { + return await this.replyRepository.find({ + where: { blab: { id: blabId } }, + relations: ['creator'], + order: { createdAt: 'DESC' } + }); + } + + async getCommentById(id: string): Promise { + return await this.replyRepository.findOne({ + where: { id }, + relations: ['creator'] + }); + } + + async updateComment(id: string, text: string): Promise { + const reply = await this.getCommentById(id); + if (!reply) { + throw new Error('Reply not found'); + } + + reply.text = text; + return await this.replyRepository.save(reply); + } + + async deleteComment(id: string): Promise { + const reply = await this.getCommentById(id); + if (!reply) { + throw new Error('Reply not found'); + } + + await this.replyRepository.softDelete(id); + } +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/services/PostService.ts b/platforms/blabsy-api/src/services/PostService.ts new file mode 100644 index 00000000..d93881f6 --- /dev/null +++ b/platforms/blabsy-api/src/services/PostService.ts @@ -0,0 +1,92 @@ +import { AppDataSource } from "../database/data-source"; +import { Blab } from "../database/entities/Blab"; +import { User } from "../database/entities/User"; +import { In } from "typeorm"; + +interface CreateBlabData { + content: string; + images?: string[]; + hashtags?: string[]; +} + +export class PostService { + private blabRepository = AppDataSource.getRepository(Blab); + private userRepository = AppDataSource.getRepository(User); + + async getFollowingFeed(userId: string, page: number, limit: number) { + const user = await this.userRepository.findOne({ + where: { id: userId }, + relations: ["following"], + }); + + if (!user) { + throw new Error("User not found"); + } + + const followingIds = user.following.map((f: User) => f.id); + const authorIds = [...followingIds, userId]; + + const [blabs, total] = await this.blabRepository.findAndCount({ + where: { + author: { id: In(authorIds) }, + isArchived: false, + }, + relations: ["author", "likedBy", "replies", "replies.creator"], + order: { + createdAt: "DESC", + }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + blabs, + total, + page, + totalPages: Math.ceil(total / limit), + }; + } + + async createPost(userId: string, data: CreateBlabData) { + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new Error("User not found"); + } + + const blab = this.blabRepository.create({ + author: user, + content: data.content, + images: data.images || [], + hashtags: data.hashtags || [], + likedBy: [], + }); + + return await this.blabRepository.save(blab); + } + + async toggleLike(blabId: string, userId: string): Promise { + const blab = await this.blabRepository.findOne({ + where: { id: blabId }, + relations: ["likedBy"], + }); + + if (!blab) { + throw new Error("Blab not found"); + } + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new Error("User not found"); + } + + const isLiked = blab.likedBy.some((u) => u.id === userId); + + if (isLiked) { + blab.likedBy = blab.likedBy.filter((u) => u.id !== userId); + } else { + blab.likedBy.push(user); + } + + return await this.blabRepository.save(blab); + } +} diff --git a/platforms/blabsy-api/src/services/UserService.ts b/platforms/blabsy-api/src/services/UserService.ts new file mode 100644 index 00000000..4e2a93a0 --- /dev/null +++ b/platforms/blabsy-api/src/services/UserService.ts @@ -0,0 +1,139 @@ +import { AppDataSource } from "../database/data-source"; +import { User } from "../database/entities/User"; +import { Blab } from "../database/entities/Blab"; +import { signToken } from "../utils/jwt"; +import { Like } from "typeorm"; + +export class UserService { + private userRepository = AppDataSource.getRepository(User); + private blabRepository = AppDataSource.getRepository(Blab); + + async createBlankUser(ename: string): Promise { + const user = this.userRepository.create({ + ename, + isVerified: false, + isPrivate: false, + isArchived: false, + }); + + return await this.userRepository.save(user); + } + + async findOrCreateUser(ename: string): Promise<{ user: User; token: string }> { + let user = await this.userRepository.findOne({ + where: { ename }, + }); + + if (!user) { + user = await this.createBlankUser(ename); + } + + const token = signToken({ userId: user.id }); + return { user, token }; + } + + async findById(id: string): Promise { + return await this.userRepository.findOneBy({ id }); + } + + searchUsers = async (query: string) => { + const searchQuery = query.toLowerCase(); + + return this.userRepository.find({ + where: [ + { username: Like(`%${searchQuery}%`) }, + { ename: Like(`%${searchQuery}%`) }, + ], + select: { + id: true, + username: true, + displayName: true, + bio: true, + profilePictureUrl: true, + isVerified: true, + }, + take: 10, + }); + }; + + followUser = async (followerId: string, followingId: string) => { + const follower = await this.userRepository.findOne({ + where: { id: followerId }, + relations: ["following"], + }); + + const following = await this.userRepository.findOne({ + where: { id: followingId }, + }); + + if (!follower || !following) { + throw new Error("User not found"); + } + + if (!follower.following) { + follower.following = []; + } + + if (follower.following.some((user) => user.id === followingId)) { + return follower; + } + + follower.following.push(following); + return await this.userRepository.save(follower); + }; + + async getProfileById(userId: string) { + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: { + id: true, + username: true, + displayName: true, + profilePictureUrl: true, + followers: true, + following: true, + bio: true, + }, + }); + + if (!user) return null; + + const blabs = await this.blabRepository.find({ + where: { author: { id: userId } }, + relations: ["author"], + order: { createdAt: "DESC" }, + }); + + return { + ...user, + totalBlabs: blabs.length, + blabs: blabs.map((blab) => ({ + id: blab.id, + avatar: blab.author.profilePictureUrl, + userId: blab.author.id, + username: blab.author.username, + imgUris: blab.images, + caption: blab.content, + time: blab.createdAt, + count: { + likes: blab.likedBy, + replies: blab.replies, + }, + })), + }; + } + + async updateProfile(userId: string, data: { username?: string; profilePictureUrl?: string; displayName?: string }): Promise { + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new Error("User not found"); + } + + if (data.username !== undefined) user.username = data.username; + if (data.profilePictureUrl !== undefined) user.profilePictureUrl = data.profilePictureUrl; + if (data.displayName !== undefined) user.displayName = data.displayName; + + return await this.userRepository.save(user); + } +} + diff --git a/platforms/blabsy-api/src/types/express.d.ts b/platforms/blabsy-api/src/types/express.d.ts new file mode 100644 index 00000000..8ad6ec48 --- /dev/null +++ b/platforms/blabsy-api/src/types/express.d.ts @@ -0,0 +1,9 @@ +import { User } from "../database/entities/User"; + +declare global { + namespace Express { + interface Request { + user?: User; + } + } +} \ No newline at end of file diff --git a/platforms/blabsy-api/src/utils/jwt.ts b/platforms/blabsy-api/src/utils/jwt.ts new file mode 100644 index 00000000..4cebfb90 --- /dev/null +++ b/platforms/blabsy-api/src/utils/jwt.ts @@ -0,0 +1,15 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; + +export const signToken = (payload: any): string => { + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); +}; + +export const verifyToken = (token: string): any => { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + throw new Error('Invalid token'); + } +}; \ No newline at end of file diff --git a/platforms/blabsy-api/tsconfig.json b/platforms/blabsy-api/tsconfig.json new file mode 100644 index 00000000..733170be --- /dev/null +++ b/platforms/blabsy-api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "moduleResolution": "node", + "baseUrl": "./src", + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/platforms/metagram/.gitignore b/platforms/blabsy/.gitignore similarity index 100% rename from platforms/metagram/.gitignore rename to platforms/blabsy/.gitignore diff --git a/platforms/metagram/.npmrc b/platforms/blabsy/.npmrc similarity index 100% rename from platforms/metagram/.npmrc rename to platforms/blabsy/.npmrc diff --git a/platforms/metagram/.prettierignore b/platforms/blabsy/.prettierignore similarity index 100% rename from platforms/metagram/.prettierignore rename to platforms/blabsy/.prettierignore diff --git a/platforms/metagram/.prettierrc b/platforms/blabsy/.prettierrc similarity index 100% rename from platforms/metagram/.prettierrc rename to platforms/blabsy/.prettierrc diff --git a/platforms/metagram/.storybook/main.ts b/platforms/blabsy/.storybook/main.ts similarity index 100% rename from platforms/metagram/.storybook/main.ts rename to platforms/blabsy/.storybook/main.ts diff --git a/platforms/metagram/.storybook/preview.ts b/platforms/blabsy/.storybook/preview.ts similarity index 100% rename from platforms/metagram/.storybook/preview.ts rename to platforms/blabsy/.storybook/preview.ts diff --git a/platforms/metagram/README.md b/platforms/blabsy/README.md similarity index 100% rename from platforms/metagram/README.md rename to platforms/blabsy/README.md diff --git a/platforms/metagram/eslint.config.js b/platforms/blabsy/eslint.config.js similarity index 100% rename from platforms/metagram/eslint.config.js rename to platforms/blabsy/eslint.config.js diff --git a/platforms/metagram/messages/en.json b/platforms/blabsy/messages/en.json similarity index 100% rename from platforms/metagram/messages/en.json rename to platforms/blabsy/messages/en.json diff --git a/platforms/metagram/messages/es.json b/platforms/blabsy/messages/es.json similarity index 100% rename from platforms/metagram/messages/es.json rename to platforms/blabsy/messages/es.json diff --git a/platforms/metagram/package.json b/platforms/blabsy/package.json similarity index 95% rename from platforms/metagram/package.json rename to platforms/blabsy/package.json index 79994d93..f35eb2d6 100644 --- a/platforms/metagram/package.json +++ b/platforms/blabsy/package.json @@ -52,6 +52,9 @@ "dependencies": { "-": "^0.0.1", "D": "^1.0.0", + "axios": "^1.6.7", + "moment": "^2.30.1", + "svelte-qrcode": "^1.0.1", "tailwind-merge": "^3.0.2" } } diff --git a/platforms/metagram/project.inlang/.gitignore b/platforms/blabsy/project.inlang/.gitignore similarity index 100% rename from platforms/metagram/project.inlang/.gitignore rename to platforms/blabsy/project.inlang/.gitignore diff --git a/platforms/metagram/project.inlang/project_id b/platforms/blabsy/project.inlang/project_id similarity index 100% rename from platforms/metagram/project.inlang/project_id rename to platforms/blabsy/project.inlang/project_id diff --git a/platforms/metagram/project.inlang/settings.json b/platforms/blabsy/project.inlang/settings.json similarity index 100% rename from platforms/metagram/project.inlang/settings.json rename to platforms/blabsy/project.inlang/settings.json diff --git a/platforms/metagram/src/app.css b/platforms/blabsy/src/app.css similarity index 100% rename from platforms/metagram/src/app.css rename to platforms/blabsy/src/app.css diff --git a/platforms/metagram/src/app.d.ts b/platforms/blabsy/src/app.d.ts similarity index 100% rename from platforms/metagram/src/app.d.ts rename to platforms/blabsy/src/app.d.ts diff --git a/platforms/metagram/src/app.html b/platforms/blabsy/src/app.html similarity index 100% rename from platforms/metagram/src/app.html rename to platforms/blabsy/src/app.html diff --git a/platforms/metagram/src/hooks.server.ts b/platforms/blabsy/src/hooks.server.ts similarity index 100% rename from platforms/metagram/src/hooks.server.ts rename to platforms/blabsy/src/hooks.server.ts diff --git a/platforms/metagram/src/hooks.ts b/platforms/blabsy/src/hooks.ts similarity index 100% rename from platforms/metagram/src/hooks.ts rename to platforms/blabsy/src/hooks.ts diff --git a/platforms/blabsy/src/lib/components/CreatePostModal.svelte b/platforms/blabsy/src/lib/components/CreatePostModal.svelte new file mode 100644 index 00000000..e966f5ac --- /dev/null +++ b/platforms/blabsy/src/lib/components/CreatePostModal.svelte @@ -0,0 +1,116 @@ + + +
+
+
+

Create Post

+ +
+ +
+ diff --git a/platforms/metagram/src/lib/ui/index.ts b/platforms/metagram/src/lib/ui/index.ts deleted file mode 100644 index 6d267a48..00000000 --- a/platforms/metagram/src/lib/ui/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default as Button } from "./Button/Button.svelte"; -export { default as Avatar } from "./Avatar/Avatar.svelte"; -export { default as Input } from "./Input/Input.svelte"; -export { default as Select } from "./Select/Select.svelte"; -export { default as Label } from "./Label/Label.svelte"; -export { default as Toggle } from "./Toggle/Toggle.svelte"; -export { default as Helper } from "./Helper/Helper.svelte"; -export { default as InputRadio } from "./InputRadio/InputRadio.svelte"; -export { default as Textarea } from "./Textarea/Textarea.svelte"; diff --git a/platforms/metagram/src/lib/utils/index.ts b/platforms/metagram/src/lib/utils/index.ts deleted file mode 100644 index b73a86d4..00000000 --- a/platforms/metagram/src/lib/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./mergeClasses"; -export * from "./clickOutside"; -export * from "./memoryHelper"; diff --git a/platforms/metagram/src/routes/(protected)/+layout.svelte b/platforms/metagram/src/routes/(protected)/+layout.svelte deleted file mode 100644 index be238757..00000000 --- a/platforms/metagram/src/routes/(protected)/+layout.svelte +++ /dev/null @@ -1,157 +0,0 @@ - - -
- alert('adas')} /> -
- {#if route === '/profile/post'} - - {:else} -
alert('Ads')} - options={[ - { name: 'Report', handler: () => alert('report') }, - { name: 'Clear chat', handler: () => alert('clear') } - ]} - /> - {/if} - {@render children()} -
- {#if route === '/home' || route === '/messages'} - - {/if} - - {#if route !== `/messages/${idFromParams}`} - - {/if} -
diff --git a/platforms/metagram/src/routes/(protected)/discover/+page.svelte b/platforms/metagram/src/routes/(protected)/discover/+page.svelte deleted file mode 100644 index 86c385a0..00000000 --- a/platforms/metagram/src/routes/(protected)/discover/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
- - {#if searchValue} -
    - {#each { length: 5 } as _} -
  • - alert('Adsad')} - /> -
  • - {/each} -
- {/if} -
diff --git a/platforms/metagram/src/routes/(protected)/messages/+page.svelte b/platforms/metagram/src/routes/(protected)/messages/+page.svelte deleted file mode 100644 index 375a657d..00000000 --- a/platforms/metagram/src/routes/(protected)/messages/+page.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
- - {#each { length: 6 } as _, i} - goto(`/messages/${i}`)} - /> - goto(`/messages/${i}`)} - /> - {/each} -
diff --git a/platforms/metagram/src/routes/(protected)/messages/[id]/+page.svelte b/platforms/metagram/src/routes/(protected)/messages/[id]/+page.svelte deleted file mode 100644 index 8e6ed045..00000000 --- a/platforms/metagram/src/routes/(protected)/messages/[id]/+page.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - -
- {#each { length: 12 } as _} - - - - - - - - - - - - {/each} - - alert('sent')} - /> -
diff --git a/platforms/metagram/src/routes/(protected)/post/+page.svelte b/platforms/metagram/src/routes/(protected)/post/+page.svelte deleted file mode 100644 index a921b393..00000000 --- a/platforms/metagram/src/routes/(protected)/post/+page.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
- { - if (uploadedImages.value) - uploadedImages.value = uploadedImages.value.filter((img, index) => { - revokeImageUrls([img]); - return index !== i - }); - }} - /> - -