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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -114,20 +114,44 @@ Go to <http://localhost:3000/sign-up> 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

Expand Down
19 changes: 19 additions & 0 deletions deployment/nginx.conf
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
40 changes: 17 additions & 23 deletions docker-compose-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,37 @@ version: '3'

services:
app:
container_name: chatee
container_name: chatee_prod
build:
context: .
dockerfile: Dockerfile
image: chatee:simplito
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:
6 changes: 4 additions & 2 deletions src/app/api/boot/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down
33 changes: 22 additions & 11 deletions src/lib/db/invite-tokens/inviteTokens.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,11 +19,16 @@ export interface InviteTokenClientDTO extends Omit<InviteToken, 'hashedValue'> {
const collectionName = 'InviteTokens';

async function getCollection() {
const mongoClient = await clientPromise;
const db = mongoClient.db();
const collection = db.collection<InviteTokenDbDTO>(collectionName);
try {
const mongoClient = await connectToDatabase();
const collection = await mongoClient.db().collection<InviteTokenDbDTO>(collectionName);
return collection;
}catch (e){
console.error(collectionName);
console.error(e.message);
}


return collection;
}

export async function createInviteToken(
Expand All @@ -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(
Expand Down
57 changes: 37 additions & 20 deletions src/lib/db/mongodb.ts
Original file line number Diff line number Diff line change
@@ -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<MongoClient>;
let dbClient:MongoClient;

if (process.env.NODE_ENV === 'development') {
let globalWithMongo = global as typeof globalThis & {
_mongoClientPromise?: Promise<MongoClient>;
};
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 };
4 changes: 2 additions & 2 deletions src/lib/db/transactions/sign-up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

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,
publicKey: string,
isStaff: boolean,
tokenValue: string
) {
const client = await clientPromise;
const client = await connectToDatabase()
const session = client.startSession();
session.startTransaction();
try {
Expand Down
7 changes: 3 additions & 4 deletions src/lib/db/users/users.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,9 +11,8 @@ export interface User {
}

async function getCollection() {
const mongoClient = await clientPromise;
const db = mongoClient.db();
const collection = db.collection<User>(`users`);
const mongoClient = await connectToDatabase();
const collection = mongoClient.db().collection<User>(`users`);

return collection;
}
Expand Down
Loading