diff --git a/.env.example b/.env.example index 714d870..69ad5fe 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ # MongoDB env REPLICA_SET=rs0 -MONGODB_URI=mongodb://127.0.0.1:27017/Chatee?replicaSet=$REPLICA_SET - +MONGODB_URI=mongodb://127.0.0.1:27017/Chatee?replicaSet=$REPLICA_SET# for production mongodb://mongo/Chatee?replicaSet=rs0 # PriMX Platform env BRIDGE_URL="BRIDGE_URL" @@ -13,6 +12,5 @@ CONTEXT_ID="CONTEXT_ID" # Chatee env JWT_SALT=RANDOM_GENERATED_SECRET -NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 - +NEXT_PUBLIC_BACKEND_URL=http://localhost:3000# for production domain of your app or just https://localhost diff --git a/Dockerfile b/Dockerfile index d009ded..44e3d08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ COPY .env.production . ENV NODE_TLS_REJECT_UNAUTHORIZED="0" ENV NEXT_TELEMETRY_DISABLED 1 -RUN yarn build +RUN npm run build # If using npm comment out above and use below instead # RUN npm run build diff --git a/README.md b/README.md index 6e882dc..7ed36fa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Chatee is a simple chat application created for testing and demonstration purpos Chatee provides essential chat features, including group chats and file attachments. All the data exchanged within Chatee is end-to-end encrypted, meaning that only the end users can read (decrypt) their messages. It means that even the platform hosting -provider cannot access user data. +provider can’t access user data. Chatee categorizes users into two distinct roles: @@ -114,20 +114,44 @@ Go to and create the first Staff user. ### Creating Threads -When creating threads (chat rooms) you are given a list of all the users from your app. +When creating threads (chat rooms) you’re given a list of all the users from your app. Staff users can create chats with all the users in the app. Regular users can create chats only with Staff. -### Production Notes - -#### Alternative ways to use our docker-compose-production.yml - -1. Create the same .env file but name it **.env.production**. -2. Run docker-compose. Variable `PORT` will define on which port it will be available. - -```sh - PORT=PORT_NUMER_OF_APPLICATION docker-compose -f docker-compose-production.yml up -``` +## Production Deployments + +1. For deployment, you will need [Docker](https://docs.docker.com/engine/install/ubuntu/) with [Docker Compose](https://docs.docker.com/compose/install/) installed on your machine +2. Create PrivMX Bridge Instance using our [PriMX Bridge docker repo](https://github.com/simplito/privmx-bridge-docker) + After a setup process you will receive Bridge secrets, you will need them later. +3. Clone this repository on your machine. +4. Copy or rename `.env.example` to `.env.production`. + Using variables from step 2 fill your `.env.production` file with few modifications. + If your PrivMX Bridge doesn't have a domain assigned to it, you should pass an IP and port of your machine + instead of `localhost`. + **PrivMX Bridge must be served via https** +5. Production docker compose comes with nginx container which needs certs for HTTPS connection + To generate cert run: + ``` + cd ./deployments + mkdir -p certs && openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout certs/selfsigned.key -out certs/selfsigned.crt \ + -subj "/CN=APP_DOMAIN_NAME" + ``` + Replace `APP_DOMAIN_NAME` with domain of your app. + If you don't have a domain name, you can pass `"localhost"` instead. + + You can edit its configuration in `/deployments/nginx.conf` file, for example, if + you want to change domain name of your app. + To start your application, **run in root of your project**: + ```sh + docker-compose -f docker-compose-production.yml up -d + ``` +6. After the first startup, you will be prompted with an invitation token for the first staff user. + You can check it using: + ``` + docker logs NAME_OF_CHATEE_CONTAINER + ``` +7. Create first staff user on `/sign-up` page using your invite token ## License diff --git a/deployment/nginx.conf b/deployment/nginx.conf new file mode 100644 index 0000000..a24dce9 --- /dev/null +++ b/deployment/nginx.conf @@ -0,0 +1,19 @@ +events { } + +http { + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/selfsigned.crt; + ssl_certificate_key /etc/nginx/certs/selfsigned.key; + + location / { + proxy_pass http://app:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + } +} \ No newline at end of file diff --git a/docker-compose-production.yml b/docker-compose-production.yml index 10c09b0..a695378 100644 --- a/docker-compose-production.yml +++ b/docker-compose-production.yml @@ -2,7 +2,7 @@ version: '3' services: app: - container_name: chatee + container_name: chatee_prod build: context: . dockerfile: Dockerfile @@ -10,35 +10,29 @@ services: restart: always volumes: - myvolume:/usr/src/app - ports: - - ${PORT}:3000 depends_on: - mongo - env_file: - - .env.production + mongo: - container_name: chatee_mongo image: mongo restart: always - command: [ '--replSet', 'rs0', '--bind_ip_all', '--port', '27017' ] + command: ['--replSet', 'rs0', "--bind_ip_all"] volumes: - myvolume:/data/db healthcheck: - test: echo 'db.runCommand("ping").ok' | mongosh --quiet - interval: 30s - timeout: 10s - retries: 3 - - mongo_init: - image: mongo + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongo:27017'}]}) }" | mongosh --port 27017 --quiet + start_period: 10s + start_interval: 1s + nginx: + image: nginx:latest + container_name: chatee-nginx + restart: always + ports: + - "443:443" + volumes: + - ./deployment/nginx.conf:/etc/nginx/nginx.conf:ro + - ./deployment/certs:/etc/nginx/certs:ro depends_on: - - mongo - command: > - /bin/bash -c " - while ! echo 'try { rs.status() } catch (err) { rs.initiate({_id:\"rs0\",members:[{_id:0,host:\"mongo:27017\"}]}); }' | mongosh --host mongo --port 27017 --quiet; do - sleep 5; - done;" - restart: on-failure - + - app volumes: - myvolume: + myvolume: \ No newline at end of file diff --git a/src/app/api/boot/route.ts b/src/app/api/boot/route.ts index d6461b5..b000227 100644 --- a/src/app/api/boot/route.ts +++ b/src/app/api/boot/route.ts @@ -5,11 +5,12 @@ import { ACCESS_KEY_SECRET, BRIDGE_URL, CONTEXT_ID, - JWT_SALT, + JWT_SALT, MONGODB_URI, SOLUTION_ID } from '@utils/env'; function checkEnv(name: string, env: string | undefined) { + console.log(`[INFO] ${name} registered: ${env}`) if (!env) { console.error(`[ERROR] ${name} missing in env file`); return true; @@ -29,7 +30,8 @@ export async function GET() { incompleteEnvs = checkEnv('ACCESS KEY SECRET', ACCESS_KEY_SECRET) || incompleteEnvs; incompleteEnvs = checkEnv('JWT SALT', JWT_SALT) || incompleteEnvs; - incompleteEnvs = checkEnv('MONGO_URI', JWT_SALT) || incompleteEnvs; + incompleteEnvs = checkEnv('MONGO_URI', MONGODB_URI) || incompleteEnvs; + if (incompleteEnvs) { console.error('[ERROR] Invalid Envs'); diff --git a/src/lib/db/invite-tokens/inviteTokens.ts b/src/lib/db/invite-tokens/inviteTokens.ts index 86b849f..4e9ae95 100644 --- a/src/lib/db/invite-tokens/inviteTokens.ts +++ b/src/lib/db/invite-tokens/inviteTokens.ts @@ -1,7 +1,7 @@ 'use server'; import { ClientSession, Filter, UpdateFilter } from 'mongodb'; -import clientPromise from '../mongodb'; +import { connectToDatabase } from '../mongodb'; import { generateInviteToken } from './utils'; export interface InviteToken { @@ -19,11 +19,16 @@ export interface InviteTokenClientDTO extends Omit { const collectionName = 'InviteTokens'; async function getCollection() { - const mongoClient = await clientPromise; - const db = mongoClient.db(); - const collection = db.collection(collectionName); + try { + const mongoClient = await connectToDatabase(); + const collection = await mongoClient.db().collection(collectionName); + return collection; + }catch (e){ + console.error(collectionName); + console.error(e.message); + } + - return collection; } export async function createInviteToken( @@ -49,12 +54,18 @@ export async function getInviteTokenByValue(tokenValue: string) { } export async function getActiveInviteTokens() { - const collection = await getCollection(); - const maxCreationDate = Date.now() - 1000 * 60 * 60 * 24 * 7; - const token = await collection - .find({ isUsed: false, creationDate: { $gte: maxCreationDate } }) - .toArray(); - return token; + + try { + const collection = await getCollection(); + const maxCreationDate = Date.now() - 1000 * 60 * 60 * 24 * 7; + const token = await collection + .find({ isUsed: false, creationDate: { $gte: maxCreationDate } }) + .toArray(); + return token; + }catch (e){ + console.error("getActive"); + console.error(e.message); + } } export async function updateInviteToken( diff --git a/src/lib/db/mongodb.ts b/src/lib/db/mongodb.ts index 0552363..c4dfa90 100644 --- a/src/lib/db/mongodb.ts +++ b/src/lib/db/mongodb.ts @@ -1,27 +1,44 @@ -import { MONGODB_URI, REPLICA_SET } from '@/shared/utils/env'; -import { MongoClient, MongoClientOptions } from 'mongodb'; +import { MONGODB_URI } from '@/shared/utils/env'; +import {MongoClient, } from 'mongodb'; -const uri = MONGODB_URI; -const options: MongoClientOptions = { - replicaSet: REPLICA_SET -}; +const uri = MONGODB_URI || "mongodb://127.0.0.1:27017/Chatee?replicaSet=rs0"; // Fallback for development -let client; -let clientPromise: Promise; +let dbClient:MongoClient; -if (process.env.NODE_ENV === 'development') { - let globalWithMongo = global as typeof globalThis & { - _mongoClientPromise?: Promise; - }; +async function connectToDatabase() { + try { + if (!dbClient) { // Only create a new connection if one doesn't exist + dbClient = new MongoClient(uri,{replicaSet:"rs0",}); + await dbClient.connect(); + console.log("Connected to MongoDB"); + } else { + console.log("Reusing existing MongoDB connection") + } + return dbClient; // Return the database object - if (!globalWithMongo._mongoClientPromise) { - client = new MongoClient(uri, options); - globalWithMongo._mongoClientPromise = client.connect(); + } catch (error) { + console.error("Error connecting to MongoDB:", error); + throw error; // Re-throw the error for handling elsewhere } - clientPromise = globalWithMongo._mongoClientPromise; -} else { - client = new MongoClient(uri, options); - clientPromise = client.connect(); } -export default clientPromise; + +async function closeDatabaseConnection() { + if (dbClient) { + try { + await dbClient.close(); + console.log("MongoDB connection closed."); + dbClient = null; + } catch (err) { + console.error("Error closing MongoDB connection:", err); + } + } +} + +process.on('SIGINT', async () => { + console.log('Shutting down gracefully...'); + await closeDatabaseConnection(); + process.exit(0); +}); + +export { connectToDatabase, closeDatabaseConnection }; \ No newline at end of file diff --git a/src/lib/db/transactions/sign-up.ts b/src/lib/db/transactions/sign-up.ts index a2ede49..ec5e74d 100644 --- a/src/lib/db/transactions/sign-up.ts +++ b/src/lib/db/transactions/sign-up.ts @@ -2,9 +2,9 @@ import { addUserToContext } from '@/lib/endpoint-api/utils'; import { expireInviteToken } from '../invite-tokens/inviteTokens'; -import clientPromise from '../mongodb'; import { createUser } from '../users/users'; import { CONTEXT_ID } from '@utils/env'; +import { connectToDatabase } from '@lib/db/mongodb'; export async function registerUser( username: string, @@ -12,7 +12,7 @@ export async function registerUser( isStaff: boolean, tokenValue: string ) { - const client = await clientPromise; + const client = await connectToDatabase() const session = client.startSession(); session.startTransaction(); try { diff --git a/src/lib/db/users/users.ts b/src/lib/db/users/users.ts index 5efe86f..f62fcd6 100644 --- a/src/lib/db/users/users.ts +++ b/src/lib/db/users/users.ts @@ -1,8 +1,8 @@ 'use server'; import { ClientSession, Filter } from 'mongodb'; -import clientPromise from '../mongodb'; import { CredentialError } from '@/lib/errors/credentialError'; +import { connectToDatabase } from '@lib/db/mongodb'; export interface User { username: string; @@ -11,9 +11,8 @@ export interface User { } async function getCollection() { - const mongoClient = await clientPromise; - const db = mongoClient.db(); - const collection = db.collection(`users`); + const mongoClient = await connectToDatabase(); + const collection = mongoClient.db().collection(`users`); return collection; } diff --git a/src/lib/endpoint-api/utils.ts b/src/lib/endpoint-api/utils.ts index 6f648b2..12a9d45 100644 --- a/src/lib/endpoint-api/utils.ts +++ b/src/lib/endpoint-api/utils.ts @@ -1,39 +1,41 @@ import { ACCESS_KEY_ID, ACCESS_KEY_SECRET, API_URL } from '@/shared/utils/env'; export async function addUserToContext(userId: string, pubKey: string, contextId: string) { - const accessToken = await getAccessToken(); - const requestBody = { - jsonrpc: '2.0', - id: 128, - method: 'context/addUserToContext', - params: { - contextId: contextId, - userId, - userPubKey: pubKey - } - }; + try { + const accessToken = await getAccessToken(); + const requestBody = { + jsonrpc: '2.0', + id: 128, + method: 'context/addUserToContext', + params: { + contextId: contextId, + userId, + userPubKey: pubKey + } + }; + const addToContextRequest = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}` + }, + body: JSON.stringify(requestBody) + }); + const response = await addToContextRequest.json(); - const addToContextRequest = await fetch(API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}` - }, - body: JSON.stringify(requestBody) - }); - - const response = await addToContextRequest.json(); + if ('error' in response) { + console.error(response.error); + throw new Error('Unable to register user in context'); + } - if ('error' in response) { - console.error(response.error); + if (addToContextRequest.status === 200) { + return; + } + } catch (e) { + console.error('Unable to register user in context'); + console.error(e); throw new Error('Unable to register user in context'); } - - if (addToContextRequest.status === 200) { - return; - } - - throw new Error('Error adding user to context'); } async function getAccessToken() {