Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a2d7051
MVP for chatting, setup for phase 2
Samh006 Dec 1, 2025
f21cf6c
phase 2 done, all features set up
Samh006 Dec 2, 2025
098daec
addded a readme for refactoring purposes
Samh006 Dec 11, 2025
227ac84
merged main into messaging_backend to unify the branches, wwill start…
Samh006 Dec 11, 2025
f78c103
invitation based system setup
Samh006 Dec 25, 2025
c0526e9
added due date functionality
Samh006 Dec 25, 2025
8b3fb46
made a script to create the first super admin. python backend/make_su…
Samh006 Dec 25, 2025
a2b4805
bug fixes and testing, passed
Samh006 Jan 1, 2026
7bbd0d7
Revert "API documentation has beenn added (SWAGGER/OpenAPI)"
Aliz275 Jan 4, 2026
a13b4e8
Reapply "API documentation has beenn added (SWAGGER/OpenAPI)"
Aliz275 Jan 4, 2026
0963f37
invitation feature added. aswell as file for token called accept-invi…
Aliz275 Jan 5, 2026
818eced
displayed the invites and ability to delete
Aliz275 Jan 6, 2026
a1414ad
Revert "displayed the invites and ability to delete"
Aliz275 Jan 6, 2026
8f3a758
invitation_routes.py tweaked
Aliz275 Jan 6, 2026
df5bc2b
fully working invitation, token, new password,
Aliz275 Jan 6, 2026
2d680e5
introduced rate limiting
Samh006 Jan 8, 2026
867cadd
enhanced rate limiting (10 per min), removed hard coded flask secret …
Samh006 Jan 9, 2026
d80fcf2
introduced schema validation, applied to routes
Samh006 Jan 9, 2026
d74b784
moved hard coded api base url to env variable, added flask talisman t…
Samh006 Jan 9, 2026
5dbcd26
dependancy issues fixed and servers not work, password encryption nee…
Samh006 Jan 9, 2026
eee03c2
passwod hashing fixed. ny existing users who were created via an invi…
Samh006 Jan 11, 2026
71a08ae
fix error in frontend id part
Aliz275 Jan 12, 2026
9faf99b
link for token added
Aliz275 Jan 12, 2026
1164dfd
frontend files added to, messaging feature
Aliz275 Jan 16, 2026
90d04f1
fixed perms for super admin
Samh006 Jan 16, 2026
1ab446d
foundational sec and org management setup
Samh006 Jan 16, 2026
a78f7e1
secured assignment management
Samh006 Jan 16, 2026
f2b1e4d
secured messaging
Samh006 Jan 16, 2026
53bcdc3
package.json
Samh006 Jan 16, 2026
130a920
#1 frontend message feature
Aliz275 Jan 19, 2026
c5f89c6
#2 messaging part
Aliz275 Jan 19, 2026
2b58055
removed org restriction from super admin
Samh006 Jan 19, 2026
dcc1c6d
test
Aliz275 Jan 19, 2026
cbf76c3
fix socket error
Aliz275 Jan 19, 2026
59be182
disabled rate limiting
Samh006 Jan 19, 2026
b2bdbc3
some changes messaginge
Aliz275 Jan 19, 2026
91c5e59
some changes messaginge
Aliz275 Jan 19, 2026
dbf5fcf
misssed file
Aliz275 Jan 19, 2026
7e75ebd
user email shows | reload page fix
Aliz275 Jan 20, 2026
6b41f1c
group chat can be created now
Aliz275 Jan 21, 2026
a88f144
turned on native spell checking
Samh006 Jan 23, 2026
39c5bdd
removed useless db_migration
Samh006 Jan 23, 2026
6d98262
quick fix to remove db_migration from main
Samh006 Jan 23, 2026
1d0b9d3
messaging tweaks
Samh006 Jan 29, 2026
d535f1f
typing indicator set up (typing/not typing)
Samh006 Jan 29, 2026
35e05cc
dynamic chat boxes, rad/unread status setup
Samh006 Jan 29, 2026
ba51714
message_routes.py changes
Aliz275 Feb 9, 2026
3e64101
Revert " message_routes.py changes"
Aliz275 Feb 10, 2026
133f11f
chat window fixes
Aliz275 Feb 10, 2026
37d4712
remove feature for gorups
Aliz275 Feb 10, 2026
460caa7
frontend changes for qol features
Aliz275 Feb 10, 2026
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# how to set up secret key:
python -c "import secrets; print(secrets.token_hex(16))"

example: f7faaa9ab2e1b3145a527a573f3c7489

then > set FLASK_SECRET_KEY=f7faaa9ab2e1b3145a527a573f3c7489 OR $env:FLASK_SECRET_KEY=f7faaa9ab2e1b3145a527a573f3c7489 if on PSH

for now you can use: $env:FLASK_SECRET_KEY="a-secret-key-that-is-long-and-random"
once set up, python main.py


super admin login:
testemail@gmail.com
testpassword

inv URL: http://localhost:8000/api/invitations
inv URL with token: http://localhost:3000/accept-invitation?token=TOKENHERE

Known issue:
password encryption does not work as intended, will fix this asap (backend)
263 changes: 116 additions & 147 deletions backend/app/assignment_routes.py

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion backend/app/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import sqlite3
from functools import wraps
from flask import session, jsonify

def get_current_user():
if 'email' not in session:
return None, None, None

conn = sqlite3.connect('database.db')
c = conn.cursor()
c.execute('SELECT id, role, organization_id FROM users WHERE email = ?', (session['email'],))
user = c.fetchone()
conn.close()

if not user:
return None, None, None

return user[0], user[1], user[2]

def role_required(allowed_roles):
def decorator(f):
@wraps(f)
Expand All @@ -14,4 +30,4 @@ def decorated_function(*args, **kwargs):

return f(*args, **kwargs)
return decorated_function
return decorator
return decorator
24 changes: 0 additions & 24 deletions backend/app/db_migrations.py

This file was deleted.

57 changes: 57 additions & 0 deletions backend/app/db_setup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#backend/app/db_setup.py

import sqlite3
import os

Expand Down Expand Up @@ -96,6 +98,61 @@ def initialize_database():
FOREIGN KEY (graded_by) REFERENCES users (id)
)''')

# --- Invitations ---
c.execute('''
CREATE TABLE IF NOT EXISTS invitations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
role TEXT NOT NULL,
organization_id INTEGER NOT NULL,
created_by INTEGER,
expires_at TIMESTAMP NOT NULL,
is_used INTEGER DEFAULT 0,
FOREIGN KEY (organization_id) REFERENCES organizations (id),
FOREIGN KEY (created_by) REFERENCES users (id)
)
''')

# --- Messaging Tables ---
c.execute('''CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
is_group_chat INTEGER NOT NULL DEFAULT 0,
created_by_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by_id) REFERENCES users (id)
)''')

c.execute('''CREATE TABLE IF NOT EXISTS conversation_participants (
user_id INTEGER NOT NULL,
conversation_id INTEGER NOT NULL,
last_read_timestamp TIMESTAMP,
PRIMARY KEY (user_id, conversation_id),
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (conversation_id) REFERENCES conversations (id)
)''')

c.execute('''CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id INTEGER NOT NULL,
sender_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
is_deleted INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (conversation_id) REFERENCES conversations (id),
FOREIGN KEY (sender_id) REFERENCES users (id)
)''')

c.execute('''CREATE TABLE IF NOT EXISTS message_read_status (
message_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
PRIMARY KEY (message_id, user_id),
FOREIGN KEY (message_id) REFERENCES messages (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)''')

conn.commit()
conn.close()
print(f"Database initialized at {DB_PATH}")
Expand Down
2 changes: 1 addition & 1 deletion backend/app/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def create_app():
app = Flask(__name__)
app.secret_key = "supersecretkey"

CORS(app, supports_credentials=True)
CORS(app, supports_credentials=True, origins="http://localhost:3000")

# Initialize DB (creates default org + default admin)
initialize_database()
Expand Down
24 changes: 23 additions & 1 deletion backend/app/insert_test_data.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# backend/app/insert_test_data.py
import sqlite3
import bcrypt
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.db_setup import initialize_database

DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'database.db')

def insert_test_data():
conn = sqlite3.connect('database.db')
initialize_database()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()

# --- Create a test organization ---
Expand Down Expand Up @@ -66,6 +73,21 @@ def insert_test_data():
""", (3, "Personal Assignment", "Visible to employee1@test.com only", user_ids["admin@test.com"], 0))
c.execute("INSERT OR IGNORE INTO user_assignments (user_id, assignment_id) VALUES (?, ?)", (employee1_id, 3))

# --- Create a test conversation ---
c.execute("INSERT OR IGNORE INTO conversations (id, name, is_group_chat, created_by_id) VALUES (?, ?, ?, ?)",
(1, "Test Group Chat", 1, user_ids["manager@test.com"]))
c.execute("INSERT OR IGNORE INTO conversation_participants (conversation_id, user_id) VALUES (?, ?)", (1, user_ids["manager@test.com"]))
c.execute("INSERT OR IGNORE INTO conversation_participants (conversation_id, user_id) VALUES (?, ?)", (1, user_ids["employee1@test.com"]))
c.execute("INSERT OR IGNORE INTO conversation_participants (conversation_id, user_id) VALUES (?, ?)", (1, user_ids["employee2@test.com"]))

# --- Create test messages ---
c.execute("INSERT OR IGNORE INTO messages (conversation_id, sender_id, content) VALUES (?, ?, ?)",
(1, user_ids["manager@test.com"], "Hello team!"))
c.execute("INSERT OR IGNORE INTO messages (conversation_id, sender_id, content) VALUES (?, ?, ?)",
(1, user_ids["employee1@test.com"], "Hello manager!"))
c.execute("INSERT OR IGNORE INTO messages (conversation_id, sender_id, content) VALUES (?, ?, ?)",
(1, user_ids["employee2@test.com"], "Hello!"))

conn.commit()
conn.close()

Expand Down
174 changes: 174 additions & 0 deletions backend/app/invitation_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#backend/app/invitation_routes.py
import sqlite3
import secrets
from datetime import datetime, timedelta
from flask import request, jsonify, session
import bcrypt
from app.auth import role_required

# -------------------------
# Helper DB Connection
# -------------------------
def get_db_connection():
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn

# -------------------------
# Initialize Invitation Routes
# -------------------------
def init_invitation_routes(app):

# -------------------------
# CORS PREFLIGHT (OPTIONS)
# -------------------------
@app.route('/api/invitations', methods=['OPTIONS'])
@app.route('/api/invitations/<int:invitation_id>', methods=['OPTIONS'])
def invitations_options(invitation_id=None):
return jsonify({}), 200

# -------------------------
# CREATE INVITATION
# -------------------------
@app.route('/api/invitations', methods=['POST'])
@role_required(['super_admin'])
def create_invitation():
data = request.get_json()
email = data.get('email')
role = data.get('role')
organization_id = data.get('organization_id')

if not all([email, role, organization_id]):
return jsonify({'message': 'Email, role, and organization ID are required'}), 400

token = secrets.token_urlsafe(16)
expires_at = datetime.now() + timedelta(days=7)
created_by = session.get('user_id', 0)

conn = get_db_connection()
c = conn.cursor()
c.execute("""
INSERT INTO invitations
(email, token, role, organization_id, created_by, expires_at, is_used)
VALUES (?, ?, ?, ?, ?, ?, 0)
""", (email, token, role, organization_id, created_by, expires_at))
conn.commit()
conn.close()

invite_link = f"http://localhost:3000/accept-invitation?token={token}"

return jsonify({
"message": "Invitation created successfully",
"token": token,
"invite_link": invite_link
}), 201

# -------------------------
# GET ALL INVITATIONS
# -------------------------
@app.route('/api/invitations', methods=['GET'])
@role_required(['super_admin'])
def get_invitations():
conn = get_db_connection()
c = conn.cursor()
c.execute('SELECT id, email, role, organization_id, expires_at, is_used FROM invitations ORDER BY id DESC')
invitations = [dict(row) for row in c.fetchall()]
conn.close()
return jsonify({'invitations': invitations}), 200

# -------------------------
# DELETE INVITATION
# -------------------------
@app.route('/api/invitations/<int:invitation_id>', methods=['DELETE'])
@role_required(['super_admin'])
def delete_invitation(invitation_id):
conn = get_db_connection()
c = conn.cursor()

# Check if invitation exists
c.execute('SELECT id FROM invitations WHERE id = ?', (invitation_id,))
inv = c.fetchone()
if not inv:
conn.close()
return jsonify({'message': 'Invitation not found'}), 404

# Delete the invitation
c.execute('DELETE FROM invitations WHERE id = ?', (invitation_id,))
conn.commit()
conn.close()

return jsonify({'message': 'Invitation deleted successfully'}), 200

# -------------------------
# VERIFY INVITATION TOKEN
# -------------------------
@app.route('/api/invitations/<token>', methods=['GET'])
def verify_invitation(token):
conn = get_db_connection()
c = conn.cursor()
c.execute('SELECT email, role, organization_id, expires_at, is_used FROM invitations WHERE token = ?', (token,))
invitation = c.fetchone()
conn.close()

if not invitation:
return jsonify({'message': 'Invalid token'}), 404

expires_at = invitation['expires_at']
if isinstance(expires_at, str):
expires_at = datetime.strptime(expires_at, '%Y-%m-%d %H:%M:%S.%f')

if expires_at < datetime.now():
return jsonify({'message': 'Token has expired'}), 400

if invitation['is_used']:
return jsonify({'message': 'Token has already been used'}), 400

return jsonify({
'email': invitation['email'],
'role': invitation['role'],
'organization_id': invitation['organization_id']
}), 200

# -------------------------
# REGISTER USER FROM INVITATION
# -------------------------
@app.route('/api/register', methods=['POST'])
def register_from_invitation():
data = request.get_json()
email = data.get("email")
password = data.get("password")
role = data.get("role")
organization_id = data.get("organization_id")
token = data.get("token")

if not all([email, password, role, organization_id, token]):
return jsonify({"message": "All fields are required"}), 400

conn = get_db_connection()
c = conn.cursor()

# Check invitation
c.execute("SELECT is_used FROM invitations WHERE token = ?", (token,))
inv = c.fetchone()
if not inv:
conn.close()
return jsonify({"message": "Invalid invitation token"}), 404
if inv['is_used']:
conn.close()
return jsonify({"message": "Invitation already used"}), 400

# Hash password
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

# Create user
c.execute(
"INSERT INTO users (email, password, role, organization_id) VALUES (?, ?, ?, ?)",
(email, hashed_password, role, organization_id)
)

# Mark invitation as used
c.execute("UPDATE invitations SET is_used = 1 WHERE token = ?", (token,))
conn.commit()
conn.close()

return jsonify({"message": "User registered successfully"}), 201
Loading