Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
013705b
add user invite to create user and token
hamidizf Mar 13, 2026
745a677
migration
hamidizf Mar 13, 2026
dc70a57
password validator, check if user can authenticate
hamidizf Mar 13, 2026
5237d97
change sign up page to accept invite
hamidizf Mar 13, 2026
a5fd5a7
add password validation
hamidizf Mar 13, 2026
a143191
add path
hamidizf Mar 13, 2026
a889f79
show if a user is active
hamidizf Mar 14, 2026
59a7504
send email on celery
hamidizf Mar 14, 2026
636dd3b
added is deleted to deactivate account
hamidizf Mar 14, 2026
627361a
deactivate and send email
hamidizf Mar 14, 2026
3c8c7fd
update settings
hamidizf Mar 14, 2026
c3171f4
deactivate
hamidizf Mar 14, 2026
976e210
name celery so they don't get mixed up
hamidizf Mar 14, 2026
830da7d
update manage_local
hamidizf Mar 14, 2026
d4c6fd7
passowrd reset
hamidizf Mar 15, 2026
323659f
reset page
hamidizf Mar 15, 2026
2bf7a2f
reset password backend
hamidizf Mar 15, 2026
7f27a61
remove signup
hamidizf Mar 15, 2026
23f6191
import functions
hamidizf Mar 15, 2026
a109fe3
remove sign up link
hamidizf Mar 15, 2026
fea9c37
updated readme
hamidizf Mar 15, 2026
6e60a4f
forgot password and reset password
hamidizf Mar 15, 2026
1280d49
remove home page and add main as default
hamidizf Mar 15, 2026
18ffcf6
sign in button on main
hamidizf Mar 15, 2026
fbf4bdc
update user profile
hamidizf Mar 15, 2026
b262f61
sign in on closed bar
hamidizf Mar 15, 2026
8001602
update tests
hamidizf Mar 15, 2026
aeb6268
remove signup
hamidizf Mar 15, 2026
f2be225
resend invite
hamidizf Mar 15, 2026
d90fb2b
UI
hamidizf Mar 15, 2026
cb7ef7f
removed sign in button on left bar
hamidizf Mar 16, 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
9 changes: 6 additions & 3 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxx
```bash
pip install -r requirements.txt
```
4. Open **4 terminals**, make sure the **venv is activated in each**, and run these commands (from `DomainX/src/backend`):
4. Open **5 terminals**, make sure the **venv is activated in each**, and run these commands (from `DomainX/src/backend`):

**Terminal 1 — Redis**
```bash
Expand All @@ -85,15 +85,18 @@ GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxx
```bash
python manage_local.py dev
```
**Terminal 3 — Celery worker (default queue)**
**Terminal 3 — Celery worker (analysis queue)**
```bash
python manage_local.py worker
```
**Terminal 4 — Celery worker (gitstats queue)**
```bash
python manage_local.py worker_gitstats
```

**Terminal 5 — Celery worker (email queue)**
```bash
python manage_local.py email
```

The API should now be running at: `http://localhost:8000/`

Expand Down
17 changes: 17 additions & 0 deletions src/backend/DomainX/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ def env_bool(name: str, default: bool = False) -> bool:
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{
'NAME': 'users.password_validators.StrongPasswordValidator',
},
]
LOGGING = {
"version": 1,
Expand Down Expand Up @@ -272,7 +275,21 @@ def env_bool(name: str, default: bool = False) -> bool:
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": False,
}
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")

EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND",
"django.core.mail.backends.console.EmailBackend"
)

DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "accounts@domainx.local")

EMAIL_HOST = os.getenv("EMAIL_HOST", "")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587))
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = env_bool("EMAIL_USE_TLS", default=True)
EMAIL_USE_SSL = env_bool("EMAIL_USE_SSL", default=False)

STATIC_URL = 'static/'

Expand Down
2 changes: 1 addition & 1 deletion src/backend/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger = get_task_logger("api.tasks.analyze_repo")


@shared_task(bind=True)
@shared_task(bind=True, queue="analysis")
def analyze_repo_task(self, library_id: str, repo_url: str):
task_id = getattr(self.request, "id", None)

Expand Down
13 changes: 12 additions & 1 deletion src/backend/manage_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,25 @@ def main():
if cmd == "worker":
if not os.getenv("CELERY_BROKER_URL"):
print("CELERY_BROKER_URL is not set. Check backend/.env")
run(["celery", "-A", "DomainX", "worker", "-l", "info", "-P", "solo", "-Q", "celery"])
run(["celery", "-A", "DomainX", "worker", "-l", "info", "-P", "solo", "-Q", "celery_analysis"])
return

if cmd == "worker_gitstats":
if not os.getenv("CELERY_BROKER_URL"):
print("CELERY_BROKER_URL is not set. Check backend/.env")
run(["celery", "-A", "DomainX", "worker", "-l", "info", "-P", "solo", "-Q", "gitstats"])
return
if cmd == "worker":
if not os.getenv("CELERY_BROKER_URL"):
print("CELERY_BROKER_URL is not set. Check backend/.env")
run(["celery", "-A", "DomainX", "worker", "-l", "info", "-P", "solo", "-Q", "analysis"])
return

if cmd == "email":
if not os.getenv("CELERY_BROKER_URL"):
print("CELERY_BROKER_URL is not set. Check backend/.env")
run(["celery", "-A", "DomainX", "worker", "-l", "info", "-P", "solo", "-Q", "email"])
return

if cmd == "loaddata":
if len(sys.argv) < 3:
Expand Down
14 changes: 10 additions & 4 deletions src/backend/users/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
User = get_user_model()

class EmailOrUsernameBackend(ModelBackend):

def authenticate(self, request, username=None, password=None, **kwargs):
if username is None or password is None:

if not username or not password:
return None

username = username.lower()

try:
user = User.objects.get(Q(email__iexact=username) | Q(username__iexact=username))
user = User.objects.get(
Q(email__iexact=username) | Q(username__iexact=username)
)
except User.DoesNotExist:
return None

if user.check_password(password):
if user.check_password(password) and self.user_can_authenticate(user):
return user

return None
return None
18 changes: 18 additions & 0 deletions src/backend/users/migrations/0002_alter_customuser_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-03-12 23:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('user', 'User'), ('admin', 'Admin'), ('superadmin', 'Superadmin')], default='admin', max_length=20),
),
]
28 changes: 28 additions & 0 deletions src/backend/users/migrations/0003_userinvite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-03-12 23:05

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0002_alter_customuser_role'),
]

operations = [
migrations.CreateModel(
name='UserInvite',
fields=[
('invite_ID', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('token_hash', models.CharField(max_length=64, unique=True)),
('expires_at', models.DateTimeField()),
('used_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('invited_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_invites', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to=settings.AUTH_USER_MODEL)),
],
),
]
18 changes: 18 additions & 0 deletions src/backend/users/migrations/0004_customuser_is_deleted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-03-14 22:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0003_userinvite'),
]

operations = [
migrations.AddField(
model_name='customuser',
name='is_deleted',
field=models.BooleanField(default=False),
),
]
27 changes: 27 additions & 0 deletions src/backend/users/migrations/0005_passwordresettoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2026-03-15 01:26

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0004_customuser_is_deleted'),
]

operations = [
migrations.CreateModel(
name='PasswordResetToken',
fields=[
('reset_ID', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('token_hash', models.CharField(max_length=64, unique=True)),
('expires_at', models.DateTimeField()),
('used_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_reset_tokens', to=settings.AUTH_USER_MODEL)),
],
),
]
90 changes: 86 additions & 4 deletions src/backend/users/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid
import secrets
import hashlib
from datetime import timedelta
from django.conf import settings
from django.utils import timezone

class CustomUser(AbstractUser):
ROLE_CHOICES = (
Expand All @@ -9,15 +15,91 @@ class CustomUser(AbstractUser):
)

email = models.EmailField(unique=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user')

role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='admin')
is_deleted = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']

def save(self, *args, **kwargs):
if self.email:
self.email = self.email.lower()
if self.username:
self.username = self.username.lower()
super().save(*args, **kwargs)

def __str__(self):
return self.email

def get_full_name(self) -> str | None:
if self.first_name and self.last_name:
return str( self.first_name + " " + self.last_name )
return str(self.first_name + " " + self.last_name)
return None


class UserInvite(models.Model):
invite_ID = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="invites"
)
invited_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sent_invites"
)
token_hash = models.CharField(max_length=64, unique=True)
expires_at = models.DateTimeField()
used_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def is_expired(self):
return timezone.now() > self.expires_at

def is_used(self):
return self.used_at is not None

@classmethod
def create_for_user(cls, user, invited_by=None, hours_valid=24):
raw_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()

invite = cls.objects.create(
user=user,
invited_by=invited_by,
token_hash=token_hash,
expires_at=timezone.now() + timedelta(hours=hours_valid),
)
return invite, raw_token

class PasswordResetToken(models.Model):
reset_ID = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="password_reset_tokens"
)
token_hash = models.CharField(max_length=64, unique=True)
expires_at = models.DateTimeField()
used_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def is_expired(self):
return timezone.now() > self.expires_at

def is_used(self):
return self.used_at is not None

@classmethod
def create_for_user(cls, user, hours_valid=1):
raw_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()

reset = cls.objects.create(
user=user,
token_hash=token_hash,
expires_at=timezone.now() + timedelta(hours=hours_valid),
)
return reset, raw_token
23 changes: 23 additions & 0 deletions src/backend/users/password_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import re
from django.core.exceptions import ValidationError


class StrongPasswordValidator:
def validate(self, password, user=None):
if not re.search(r"[A-Z]", password):
raise ValidationError("Password must contain at least one uppercase letter.")

if not re.search(r"[a-z]", password):
raise ValidationError("Password must contain at least one lowercase letter.")

if not re.search(r"\d", password):
raise ValidationError("Password must contain at least one number.")

if not re.search(r"[^\w\s]", password):
raise ValidationError("Password must contain at least one special character.")

def get_help_text(self):
return (
"Your password must contain at least 8 characters, including one uppercase letter, "
"one lowercase letter, one number, and one special character."
)
Loading
Loading