add app user management allauth

This commit is contained in:
2025-02-01 19:16:33 +01:00
parent c13049a30f
commit e3ae8300f1
55 changed files with 1147 additions and 465 deletions

1
.env
View File

@@ -3,6 +3,7 @@ DB_USER=postgres
DB_PASSWORD=BatManu#171017 DB_PASSWORD=BatManu#171017
DB_HOST=10.211.114.101 DB_HOST=10.211.114.101
DB_PORT=5432 DB_PORT=5432
MASTER_PASSWORD=pipperepettenuse
MQTT_HOST=10.211.114.214 MQTT_HOST=10.211.114.214
MQTT_PORT=1883 MQTT_PORT=1883
MQTT_KEEPALIVE=60 MQTT_KEEPALIVE=60

1
.vscode/launch.json vendored
View File

@@ -12,7 +12,6 @@
"request": "launch", "request": "launch",
"args": [ "args": [
"runserver", "runserver",
"--noreload",
], ],
"django": true, "django": true,
"autoStartBrowser": false, "autoStartBrowser": false,

0
aseusers/__init__.py Normal file
View File

4
aseusers/admin.py Normal file
View File

@@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Profile
admin.site.register(Profile)

9
aseusers/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AseusersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'aseusers'
def ready(self):
import aseusers.signals

22
aseusers/forms.py Normal file
View File

@@ -0,0 +1,22 @@
from django.forms import ModelForm
from django import forms
from django.contrib.auth.models import User
from .models import Profile
class ProfileForm(ModelForm):
class Meta:
model = Profile
fields = ['image', 'displayname', 'info' ]
widgets = {
'image': forms.FileInput(),
'displayname' : forms.TextInput(attrs={'placeholder': 'Add display name'}),
'info' : forms.Textarea(attrs={'rows':3, 'placeholder': 'Add information'})
}
class EmailForm(ModelForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['email']

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.4 on 2025-01-31 20:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='avatars/')),
('displayname', models.CharField(blank=True, max_length=20, null=True)),
('info', models.TextField(blank=True, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

24
aseusers/models.py Normal file
View File

@@ -0,0 +1,24 @@
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = models.ImageField(upload_to='avatars/', null=True, blank=True)
displayname = models.CharField(max_length=20, null=True, blank=True)
info = models.TextField(null=True, blank=True)
def __str__(self):
return str(self.user)
@property
def name(self):
if self.displayname:
return self.displayname
return self.user.username
@property
def avatar(self):
if self.image:
return self.image.url
return f'{settings.STATIC_URL}images/avatar.svg'

37
aseusers/signals.py Normal file
View File

@@ -0,0 +1,37 @@
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
from allauth.account.models import EmailAddress
from django.contrib.auth.models import User
from .models import Profile
@receiver(post_save, sender=User)
def user_postsave(sender, instance, created, **kwargs):
user = instance
# add profile if user is created
if created:
Profile.objects.create(
user = user,
)
else:
# update allauth emailaddress if exists
try:
email_address = EmailAddress.objects.get_primary(user)
if email_address.email != user.email:
email_address.email = user.email
email_address.verified = False
email_address.save()
except:
# if allauth emailaddress doesn't exist create one
EmailAddress.objects.create(
user = user,
email = user.email,
primary = True,
verified = False
)
@receiver(pre_save, sender=User)
def user_presave(sender, instance, **kwargs):
if instance.username:
instance.username = instance.username.lower()

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/blank.html' %} {% block content %}
<div
class="container d-flex flex-column align-items-center pt-5 px-4"
style="max-width: 32rem"
>
<img
class="w-36 h-36 rounded-circle object-fit-cover mb-4"
src="{{ profile.avatar }}"
height="144"
/>
<div class="text-center">
<h1>{{ profile.name }}</h1>
<div class="text-muted mb-2 mt-n3">@{{ profile.user.username }}</div>
{% if profile.info %}
<div class="mt-4 hyphens-auto">{{ profile.info|linebreaksbr }}</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'layouts/box.html' %} {% block content %}
<h1 class="mb-4">Delete Account</h1>
<p class="mb-4">Are you sure you want to delete your account?</p>
<form method="POST" class="d-flex gap-2">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
Yes, I want to delete my account
</button>
<a class="btn btn-secondary" href="{{ request.META.HTTP_REFERER }}">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends 'layouts/box.html' %} {% block content %} {% if onboarding %}
<h1 class="mb-4">Complete your Profile</h1>
{% else %}
<h1 class="mb-4">Edit your Profile</h1>
{% endif %}
<div class="text-center d-flex flex-column align-items-center">
<img
id="avatar"
class="w-36 h-36 rounded-circle object-fit-cover my-4"
src="{{ user.profile.avatar }} "
height="144"
/>
<div class="text-center" style="max-width: 24rem">
<h1 id="displayname">{{ user.profile.displayname|default:"" }}</h1>
<div class="text-muted mb-2 mt-n3">@{{ user.username }}</div>
</div>
</div>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %} {{ form.as_p }}
<button type="submit" class="btn btn-primary">Submit</button>
{% if onboarding %}
<a class="btn btn-secondary ms-2" href="{% url 'home' %}">Skip</a>
{% else %}
<a class="btn btn-secondary ms-2" href="{{ request.META.HTTP_REFERER }}"
>Cancel</a
>
{% endif %}
</form>
<script>
// This updates the avatar
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
const image = document.querySelector("#avatar");
if (file && file.type.includes("image")) {
const url = URL.createObjectURL(file);
image.src = url;
}
});
// This updates the name
const display_nameInput = document.getElementById("id_displayname");
const display_nameOutput = document.getElementById("displayname");
display_nameInput.addEventListener("input", (event) => {
display_nameOutput.innerText = event.target.value;
});
</script>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends 'layouts/box.html' %} {% block content %}
<h1 class="mb-4">Account Settings</h1>
<table class="table">
<tbody>
<tr>
<th scope="row" class="pt-3 pb-1 fw-bold">Email address</th>
<td id="email-address" class="pt-3 pb-1 ps-3">
{% if user.email %}{{ user.email }}{% else %}No Email{% endif %}
</td>
<td class="pt-3 pb-1 ps-3">
<a
id="email-edit"
class="text-decoration-none text-primary cursor-pointer"
hx-get="{% url 'profile-emailchange' %}"
hx-target="#email-address"
hx-swap="innerHTML"
>
Edit
</a>
</td>
</tr>
<tr class="border-bottom">
<th scope="row" class="pb-3 fw-bold"></th>
<td class="pb-3 ps-3">
{% if user.emailaddress_set.first.verified %}
<span class="text-success">Verified</span>
{% else %}
<span class="text-warning">Not verified</span>
{% endif %}
</td>
<td class="pb-3 ps-3">
<a
href="{% url 'profile-emailverify' %}"
class="text-decoration-none text-primary"
>
{% if not user.emailaddress_set.first.verified %}Verify{% endif %}
</a>
</td>
</tr>
<tr class="border-bottom">
<th scope="row" class="py-3 fw-bold">Delete Account</th>
<td class="py-3 ps-3">Once deleted, account is gone. Forever.</td>
<td class="py-3 ps-3">
<a
href="{% url 'profile-delete' %}"
class="text-decoration-none text-danger"
>
Delete
</a>
</td>
</tr>
</tbody>
</table>
{% endblock %}

3
aseusers/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
aseusers/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path
from aseusers.views import *
urlpatterns = [
path('', profile_view, name="profile"),
path('edit/', profile_edit_view, name="profile-edit"),
path('onboarding/', profile_edit_view, name="profile-onboarding"),
path('settings/', profile_settings_view, name="profile-settings"),
path('emailchange/', profile_emailchange, name="profile-emailchange"),
path('emailverify/', profile_emailverify, name="profile-emailverify"),
path('delete/', profile_delete_view, name="profile-delete"),
]

93
aseusers/views.py Normal file
View File

@@ -0,0 +1,93 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from allauth.account.utils import send_email_confirmation
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout
from django.contrib.auth.models import User
from django.contrib.auth.views import redirect_to_login
from django.contrib import messages
from .forms import *
def profile_view(request, username=None):
if username:
profile = get_object_or_404(User, username=username).profile
else:
try:
profile = request.user.profile
except:
return redirect_to_login(request.get_full_path())
return render(request, 'aseusers/profile.html', {'profile':profile})
@login_required
def profile_edit_view(request):
form = ProfileForm(instance=request.user.profile)
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
if form.is_valid():
form.save()
return redirect('profile')
if request.path == reverse('profile-onboarding'):
onboarding = True
else:
onboarding = False
return render(request, 'aseusers/profile_edit.html', { 'form':form, 'onboarding':onboarding })
@login_required
def profile_settings_view(request):
return render(request, 'aseusers/profile_settings.html')
@login_required
def profile_emailchange(request):
if request.htmx:
form = EmailForm(instance=request.user)
return render(request, 'partials/email_form.html', {'form':form})
if request.method == 'POST':
form = EmailForm(request.POST, instance=request.user)
if form.is_valid():
# Check if the email already exists
email = form.cleaned_data['email']
if User.objects.filter(email=email).exclude(id=request.user.id).exists():
messages.warning(request, f'{email} is already in use.')
return redirect('profile-settings')
form.save()
# Then Signal updates emailaddress and set verified to False
# Then send confirmation email
send_email_confirmation(request, request.user)
return redirect('profile-settings')
else:
messages.warning(request, 'Form not valid')
return redirect('profile-settings')
return redirect('home')
@login_required
def profile_emailverify(request):
send_email_confirmation(request, request.user)
return redirect('profile-settings')
@login_required
def profile_delete_view(request):
user = request.user
if request.method == "POST":
logout(request)
user.delete()
messages.success(request, 'Account deleted, what a pity')
return redirect('home')
return render(request, 'aseusers/profile_delete.html')

BIN
avatars/ape2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -8,9 +8,12 @@ dependencies = [
"bcrypt>=4.2.1", "bcrypt>=4.2.1",
"cryptography>=44.0.0", "cryptography>=44.0.0",
"django>=5.1.4", "django>=5.1.4",
"django-allauth>=65.3.1",
"django-cleanup>=9.0.0",
"django-htmx>=1.21.0", "django-htmx>=1.21.0",
"gunicorn>=23.0.0", "gunicorn>=23.0.0",
"paho-mqtt>=2.1.0", "paho-mqtt>=2.1.0",
"pillow>=11.1.0",
"psycopg2-binary>=2.9.10", "psycopg2-binary>=2.9.10",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
] ]

BIN
static/images/ase-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

65
static/js/htmx.min.js vendored

File diff suppressed because one or more lines are too long

10
templates/404.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends 'layouts/blank.html' %} {% block content %}
<div class="container text-center py-5">
<h1 class="display-1">Page not found</h1>
<p class="my-4 text-muted">
Error 404: This page doesn't exist or is unavailable
</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "account/base_entrance.html" %}
{% load i18n %}
{% load allauth account %}
{% block head_title %}
{% trans "Sign In" %}
{% endblock head_title%}
{% block content %}
<h1 class="mb-4">{% trans "Sign In" %}</h1>
<p class="mb-4">
If you have not created an account yet, then please
<a
href="{% url 'account_signup' %}?next={% url 'profile-onboarding' %}"
class="text-decoration-none"
>sign up</a
>
first.
</p>
{% url 'account_login' as login_url %}
<form method="post" action="{{ login_url }}" class="mb-4">
{% csrf_token %}
<div class="mb-3">
{% for field in form %}
<div class="form-group">
{{ field }} {% if field.errors %}
<div class="text-danger">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% if redirect_field_value %}
<input
type="hidden"
name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}"
/>
{% endif %}
<button type="submit" class="btn btn-primary">{% trans "Sign In" %}</button>
</form>
{% if SOCIALACCOUNT_ENABLED %}
{% include "socialaccount/snippets/login.html" with page_layout="entrance" %}
{% endif %}
{% endblock content %}

View File

View File

@@ -0,0 +1,12 @@
{% extends 'layouts/box.html' %} {% block class %}allauth{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div class="bg-white rounded-3 p-4 p-md-5 shadow-lg">
{% block allauth_content %} {% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

35
templates/base.html Normal file
View File

@@ -0,0 +1,35 @@
{% load static %}
{% load django_htmx %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django Template">
<title>Project Title</title>
<link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" defer></script>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
{% django_htmx_script %}
<!-- Custom Styles -->
<style>
.dropdown-menu { min-width: 200px; }
</style>
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% include 'includes/messages.html' %}
{% include 'includes/header.html' %}
{% block layout %}
{% endblock %}
{% block javascript %}{% endblock %}
</body>
</html>

7
templates/home.html Normal file
View File

@@ -0,0 +1,7 @@
{% extends 'layouts/blank.html' %} {% block content %}
<div class="container py-5">
<h1>New Project</h1>
</div>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% load static %}
<header class="d-flex align-items-center justify-content-between bg-dark text-white py-3 px-4 sticky-top z-40">
<div>
<a class="d-flex align-items-center gap-2 text-decoration-none text-white" href="/">
<img class="h-6" src="{% static 'images/ase-logo.png' %}" alt="ASE" height="40"/>
<span class="text-lg fw-bold">MQTT User Management</span>
</a>
</div>
<nav class="position-relative">
<ul class="nav nav-pills align-items-center">
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-white" href="/">Home</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2 text-white" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<img class="h-8 w-8 rounded-circle object-cover" src="{{ request.user.profile.avatar }}" alt="Avatar" height="40"/>
{{ request.user.profile.name }}
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-lg" style="min-width: 200px;">
<li><a class="dropdown-item" href="{% url 'profile' %}">My Profile</a></li>
<li><a class="dropdown-item" href="{% url 'profile-edit' %}">Edit Profile</a></li>
<li><a class="dropdown-item" href="{% url 'profile-settings' %}">Settings</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Log Out</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'account_login' %}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'account_signup' %}?next={% url 'profile-onboarding' %}">Signup</a>
</li>
{% endif %}
</ul>
</nav>
</header>

View File

@@ -0,0 +1,29 @@
<messages>
{% if messages %}
<div>
{% for message in messages %}
<div
class="position-fixed top-0 start-50 translate-middle-x mt-3 z-50"
style="width: 100%; max-width: 576px"
>
<div
class="alert alert-{{ message.tags }} alert-dismissible fade show"
role="alert"
>
<div class="d-flex align-items-center">
<div class="me-3">
<div class="text-lg">{{ message }}</div>
</div>
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</messages>

View File

@@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block layout %}
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'base.html' %} {% block layout %}
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div class="bg-white rounded-3 p-4 p-md-5 shadow-lg">
{% block content %} {% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,8 @@
<button
class="btn btn-warning btn-sm"
hx-post="/user/disable/{{user.slug}}"
hx-target="#user-status"
hx-swap="innerHTML"
>
Disable
</button>

View File

@@ -0,0 +1,18 @@
<form
action="{% url 'profile-emailchange' %}"
method="post"
class="d-flex gap-2"
autocomplete="off"
>
{% csrf_token %} {% for field in form %} {{ field }} {% endfor %}
<button class="btn btn-dark" type="submit">Submit</button>
</form>
<a
hx-swap-oob="true"
id="email-edit"
href="{% url 'profile-settings' %}"
class="text-primary text-decoration-none"
>
Cancel
</a>

View File

@@ -0,0 +1,8 @@
<button
class="btn btn-warning btn-sm"
hx-post="/user/enable/{{user.slug}}"
hx-target="#user-status"
hx-swap="innerHTML"
>
Enable
</button>

View File

@@ -1,6 +1,21 @@
{% extends "base.html" %} {% load static %} {% block content %} {% extends 'layouts/blank.html' %} {% block content %}
<!-- Mostra i messaggi di errore o successo -->
{% if messages %}
<div>
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Form per la creazione dell'utente -->
<h1>Add User</h1> <h1>Add User</h1>
<form hx-post="/create_user" hx-target="#content"> <form method="post" hx-post="/create_user" hx-target="#content">
{% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label for="site" class="form-label">Site</label> <label for="site" class="form-label">Site</label>
<input type="text" class="form-control" id="site" name="site" required /> <input type="text" class="form-control" id="site" name="site" required />
@@ -29,10 +44,6 @@
<label for="topic" class="form-label">Topic</label> <label for="topic" class="form-label">Topic</label>
<input type="text" class="form-control" id="topic" name="topic" required /> <input type="text" class="form-control" id="topic" name="topic" required />
</div> </div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<input type="text" class="form-control" id="role" name="role" required />
</div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label">Password</label>
<input <input

View File

@@ -0,0 +1,60 @@
{% extends 'layouts/blank.html' %} {% block content %}
<!-- Mostra i messaggi di errore o successo -->
{% if messages %}
<div>
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<h1>Add User</h1>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="site" class="form-label">Site</label>
<input type="text" class="form-control" id="site" name="site" required />
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input
type="text"
class="form-control"
id="username"
name="username"
required
/>
</div>
<div class="mb-3">
<label for="clientId" class="form-label">Client ID</label>
<input
type="text"
class="form-control"
id="clientId"
name="clientId"
required
/>
</div>
<div class="mb-3">
<label for="topic" class="form-label">Topic</label>
<input type="text" class="form-control" id="topic" name="topic" required />
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
id="password"
name="password"
required
/>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
<!-- Risultato della creazione -->
<div id="result"></div>
{% endblock content%}

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% load static %} {% block content %} {% extends 'layouts/blank.html' %} {% block content %}
<h1>Edit User</h1> <h1>Edit User</h1>
<form hx-post="/update_user" hx-target="#content"> <form method="post">
{% csrf_token %}
<input type="hidden" name="id" value="{{ user.id }}" /> <input type="hidden" name="id" value="{{ user.id }}" />
<div class="mb-3"> <div class="mb-3">
<label for="site" class="form-label">Site</label> <label for="site" class="form-label">Site</label>
@@ -46,17 +47,6 @@
required required
/> />
</div> </div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<input
type="text"
class="form-control"
id="role"
name="role"
value="{{ user.role }}"
required
/>
</div>
<button type="submit" class="btn btn-primary">Update</button> <button type="submit" class="btn btn-primary">Update</button>
</form> </form>
{% endblock content %} {% endblock content %}

View File

@@ -0,0 +1,57 @@
{% extends 'layouts/blank.html' %} {% block content %}
<!-- Mostra i messaggi di errore o successo -->
{% if messages %}
<div>
{% for message in messages %}
<div class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<h1>List Mosquitto Users</h1>
<a href="{% url 'create_user' %}">
<button class="btn btn-primary mb-3">Add User</button>
</a>
<table class="table table-striped">
<thead>
<tr>
<th>Site</th>
<th>Username</th>
<th>Client ID</th>
<th>Topic</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.site}}</td>
<td>
<a href="{% url 'edit_user' user.slug %}">{{user.username}}</a
>
</td>
<td>{{user.client_id}}</td>
<td>{{user.topic}}</td>
<td id="user-status">
{% if user.status == "enabled" %}
<a href="{% url 'disable_user' user.slug %}">
<button class="btn btn-danger btn-sm">Disable</button>
</a>
{% else %}
<a href="{% url 'enable_user' user.slug %}">
<button class="btn btn-warning btn-sm">Enable</button>
</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5"><i> No users </i></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View File

@@ -1,4 +1,4 @@
{% extends "base.html" %} {% load static %} {% block content %} {% extends 'layouts/blank.html' %} {% block content %}
<h1>Role Permissions</h1> <h1>Role Permissions</h1>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>

63
uv.lock generated
View File

@@ -118,6 +118,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/0b/8a4ab2c02982df4ed41e29f28f189459a7eba37899438e6bea7f39db793b/Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0", size = 8276471 }, { url = "https://files.pythonhosted.org/packages/58/0b/8a4ab2c02982df4ed41e29f28f189459a7eba37899438e6bea7f39db793b/Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0", size = 8276471 },
] ]
[[package]]
name = "django-allauth"
version = "65.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/73/3a24b32df7046c5106846c0cfb3309033e2ebe00b08dfbf64c883add253a/django_allauth-65.3.1.tar.gz", hash = "sha256:e02e951b71a2753a746459f2efa114c7c72bf2cef6887dbe8607a577c0350587", size = 1547530 }
[[package]]
name = "django-cleanup"
version = "9.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/01/b15a8de8b9ec75ea157ec58f86411894ca1182305fabaee31193076e7f62/django_cleanup-9.0.0.tar.gz", hash = "sha256:bb9fb560aaf62959c81e31fa40885c36bbd5854d5aa21b90df2c7e4ba633531e", size = 17917 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/d7/a83dc87c2383e125da29948f7bccf5b30126c087a5a831316482407a960f/django_cleanup-9.0.0-py3-none-any.whl", hash = "sha256:19f8b0e830233f9f0f683b17181f414672a0f48afe3ea3cc80ba47ae40ad880c", size = 10726 },
]
[[package]] [[package]]
name = "django-htmx" name = "django-htmx"
version = "1.21.0" version = "1.21.0"
@@ -139,9 +158,12 @@ dependencies = [
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "django" }, { name = "django" },
{ name = "django-allauth" },
{ name = "django-cleanup" },
{ name = "django-htmx" }, { name = "django-htmx" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "paho-mqtt" }, { name = "paho-mqtt" },
{ name = "pillow" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
] ]
@@ -151,9 +173,12 @@ requires-dist = [
{ name = "bcrypt", specifier = ">=4.2.1" }, { name = "bcrypt", specifier = ">=4.2.1" },
{ name = "cryptography", specifier = ">=44.0.0" }, { name = "cryptography", specifier = ">=44.0.0" },
{ name = "django", specifier = ">=5.1.4" }, { name = "django", specifier = ">=5.1.4" },
{ name = "django-allauth", specifier = ">=65.3.1" },
{ name = "django-cleanup", specifier = ">=9.0.0" },
{ name = "django-htmx", specifier = ">=1.21.0" }, { name = "django-htmx", specifier = ">=1.21.0" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "paho-mqtt", specifier = ">=2.1.0" }, { name = "paho-mqtt", specifier = ">=2.1.0" },
{ name = "pillow", specifier = ">=11.1.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
] ]
@@ -188,6 +213,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 }, { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 },
] ]
[[package]]
name = "pillow"
version = "11.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 },
{ url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 },
{ url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 },
{ url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 },
{ url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 },
{ url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 },
{ url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 },
{ url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 },
{ url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 },
{ url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 },
{ url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 },
{ url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
{ url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
{ url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
{ url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
{ url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
{ url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
{ url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
{ url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
{ url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
{ url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
{ url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
{ url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
{ url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
{ url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
{ url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
{ url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
{ url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
{ url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
{ url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
]
[[package]] [[package]]
name = "psycopg2-binary" name = "psycopg2-binary"
version = "2.9.10" version = "2.9.10"

View File

@@ -26,7 +26,9 @@ SECRET_KEY = 'django-insecure-=-1c582(r8ly33q+-ljg!xb2)#$+a0ykedc4zcj-dqh&i$4$l3
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['localhost', '127.0.0.1', '*']
CSRF_TRUSTED_ORIGINS = [ 'https://*' ]
# Application definition # Application definition
@@ -38,8 +40,15 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_cleanup.apps.CleanupConfig',
# 'django.contrib.sites',
'allauth',
'allauth.account',
'django_htmx', 'django_htmx',
# My apps
'wallet_api.apps.WalletApiConfig', 'wallet_api.apps.WalletApiConfig',
'aseusers.apps.AseusersConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -50,15 +59,22 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
"django_htmx.middleware.HtmxMiddleware", "django_htmx.middleware.HtmxMiddleware",
] ]
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
ROOT_URLCONF = 'wallet.urls' ROOT_URLCONF = 'wallet.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [ BASE_DIR / "templates" ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -112,9 +128,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'it-it'
TIME_ZONE = 'UTC' TIME_ZONE = 'CET'
USE_I18N = True USE_I18N = True
@@ -127,7 +143,17 @@ USE_TZ = True
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATICFILES_DIRS = [ BASE_DIR / "static" ] STATICFILES_DIRS = [ BASE_DIR / "static" ]
# MEDIA_URL = 'media/'
# MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_REDIRECT_URL = '/'
ACCOUNT_SIGNUP_REDIRECT_URL = "{% url 'account_signup' %}?next={% url 'profile-onboarding' %}"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True

View File

@@ -1,130 +0,0 @@
"""
Django settings for wallet project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
from dotenv import dotenv_values
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Load env variable from .env
config = dotenv_values(".env")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'y@-63z+ao=w=5%%e&p)a7e=qscvqokg$ai5gyejuh5$hs*p4ij'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'wallet_api.apps.WalletApiConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'wallet.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'wallet.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config['DB_NAME'],
'USER': config['DB_USER'],
'PASSWORD': config['DB_PASSWORD'],
'HOST': config['DB_HOST'],
'PORT': config['DB_PORT'],
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@@ -16,8 +16,19 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from aseusers.views import profile_view
from wallet_api.views import *
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('wallet/', include('wallet_api.urls')), path('accounts/', include('allauth.urls')),
path('', include('wallet_api.urls')),
path('profile/', include('aseusers.urls')),
path('@<username>/', profile_view, name="profile"),
] ]
# Only used when DEBUG=True, whitenoise can serve files when DEBUG=False
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,45 +0,0 @@
from queue import Queue
from dotenv import dotenv_values
import paho.mqtt.client as mqtt
import json
import os
print(f"__init__.py eseguito in PID {os.getpid()}")
config = dotenv_values(".env")
listClients_queue = Queue()
listRoles_queue = Queue()
getClient_queue = Queue()
changeClient_queue = Queue()
command_queue_map = {
"listClients": listClients_queue,
"listRoles": listRoles_queue,
"getClient": getClient_queue,
"createClient": changeClient_queue,
"deleteClient": changeClient_queue,
"enableClient": changeClient_queue,
"disableClient": changeClient_queue
}
def on_message(client, userdata, msg):
msg_json = json.loads(msg.payload.decode("utf-8"))
command = msg_json['responses'][0]['command']
print(f"Received message: {msg.payload.decode("utf-8")} - Command: {command}")
if command in command_queue_map:
command_queue_map[command].put(msg.payload.decode("utf-8"))
def on_connect(client, userdata, flags, rc, properties):
if rc == 0:
print('Connected successfully. Properties:', properties)
client.subscribe(config['MQTT_DS_RESP_TOPIC'])
else:
print('Bad connection. Code:', rc)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
client.on_connect = on_connect
client.on_message = on_message
client.username_pw_set(config['MQTT_USER'], config['MQTT_PASSWORD'])
client.connect(config['MQTT_HOST'], int(config['MQTT_PORT']), int(config['MQTT_KEEPALIVE']))
client.loop_start()

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.1.4 on 2025-01-15 18:24 # Generated by Django 5.1.4 on 2025-01-31 22:09
from django.db import migrations, models from django.db import migrations, models
@@ -14,9 +14,8 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='MasterHash', name='MasterHash',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.CharField(default='1', max_length=10, primary_key=True, serialize=False)),
('hash', models.BinaryField()), ('hash', models.BinaryField()),
('created_at', models.DateTimeField(auto_now_add=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -26,12 +25,10 @@ class Migration(migrations.Migration):
('site', models.CharField(max_length=255)), ('site', models.CharField(max_length=255)),
('username', models.CharField(max_length=255)), ('username', models.CharField(max_length=255)),
('password', models.TextField()), ('password', models.TextField()),
('client_id', models.CharField(max_length=255)), ('client_id', models.CharField(blank=True, max_length=255)),
('topic', models.CharField(max_length=255)), ('topic', models.CharField(blank=True, max_length=255)),
('role', models.CharField(max_length=255)), ('status', models.CharField(blank=True, max_length=255)),
('acls', models.JSONField()), ('slug', models.SlugField(blank=True, unique=True)),
('status', models.CharField(max_length=255)),
('slug', models.SlugField(default='', null=True)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
], ],
options={ options={

View File

@@ -1,4 +1,5 @@
from django.db import models from django.db import models
from django.utils.text import slugify
# Create your models here. # Create your models here.
@@ -6,14 +7,17 @@ class PasswordEntry(models.Model):
site = models.CharField(max_length=255) site = models.CharField(max_length=255)
username = models.CharField(max_length=255) username = models.CharField(max_length=255)
password = models.TextField() password = models.TextField()
client_id = models.CharField(max_length=255) client_id = models.CharField(max_length=255, blank=True)
topic = models.CharField(max_length=255) topic = models.CharField(max_length=255, blank=True)
role = models.CharField(max_length=255) status = models.CharField(max_length=255, blank=True)
acls = models.JSONField() slug = models.SlugField(unique=True, blank=True)
status = models.CharField(max_length=255)
slug = models.SlugField(default="", null=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.username, self.site) # Automatically generate slug from the title
super().save(*args, **kwargs)
class Meta: class Meta:
unique_together = ('site', 'username', 'client_id') unique_together = ('site', 'username', 'client_id')
@@ -21,5 +25,8 @@ class PasswordEntry(models.Model):
return self.username return self.username
class MasterHash(models.Model): class MasterHash(models.Model):
hash = models.BinaryField() id = models.CharField(primary_key=True, max_length=10, default="1") # Unica riga con ID fisso
created_at = models.DateTimeField(auto_now_add=True) hash = models.BinaryField() # Hash bcrypt della master password
def __str__(self):
return f"MasterHash(id={self.id})"

View File

@@ -1,32 +0,0 @@
import paho.mqtt.client as mqtt
MQTT_SERVER="10.211.114.214"
MQTT_PORT=1883
MQTT_KEEPALIVE=60
MQTT_USER="pippo"
MQTT_PASSWORD="batt1l0"
MQTT_DS_TOPIC="$CONTROL/dynamic-security/v1"
def on_connect(mqtt_client, userdata, flags, rc, properties):
if rc == 0:
print('Connected successfully. Properties:', properties)
mqtt_client.subscribe(MQTT_TOPIC)
else:
print('Bad connection. Code:', rc)
def on_disconnect(mqtt_client, obj, flags, rc, properties):
print('disconnected. Code:', rc)
def on_message(mqtt_client, userdata, msg):
print(f'Received message on topic: {msg.topic} with payload: {msg.payload}')
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
client.connect(
host=MQTT_SERVER,
port=MQTT_PORT,
keepalive=MQTT_KEEPALIVE
)

View File

@@ -0,0 +1,58 @@
import paho.mqtt.client as mqtt
import json
import time
from dotenv import dotenv_values
class MosquittoDynamicSecurity:
def __init__(self):
config = dotenv_values(".env")
self.broker_url = config["MQTT_HOST"]
self.broker_port = config["MQTT_PORT"]
self.broker_keepalive = config["MQTT_KEEPALIVE"]
self.user = config["MQTT_USER"]
self.password = config["MQTT_PASSWORD"]
self.topic = config["MQTT_DS_TOPIC"]
self.resp_topic = config["MQTT_DS_RESP_TOPIC"]
self.response = None
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
self.client.on_connect = self.on_connect
self.client.on_disconnect = self.on_disconnect
self.client.on_message = self.on_message
def on_disconnect(self, mqtt_client, obj, flags, rc, properties):
print('Disconnesso da Mosquitto con codice:', rc)
def on_connect(self, client, userdata, flags, rc, properties):
if rc == 0:
print("Connesso a Mosquitto con codice:", rc)
# Sottoscriviti al topic di risposta
client.subscribe(self.resp_topic)
else:
print("Errore di connessione a Mosquitto con codice:", rc)
def on_message(self, client, userdata, msg):
print('Response:', msg.payload.decode())
# Memorizza la risposta
self.response = msg.payload.decode()
def send_command(self, command):
# Pubblica il comando sul topic di controllo
self.client.username_pw_set(self.user, self.password)
self.client.connect(self.broker_url, int(self.broker_port), int(self.broker_keepalive))
self.client.loop_start()
time.sleep(0.2)
self.client.publish(self.topic, json.dumps(command))
# Attendi la risposta (timeout di 5 secondi)
start_time = time.time()
while self.response is None and time.time() - start_time < 5:
time.sleep(0.2)
self.client.loop_stop()
self.client.disconnect()
if self.response:
return json.loads(self.response)
else:
return {"error": "Timeout: nessuna risposta da Mosquitto"}

View File

@@ -1,39 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<script src="{% static 'js/htmx.min.js' %}" defer></script>
<title>{% if title %}: {{title}} {% endif %}</title>
{% block extra_head %} {% endblock extra_head %} {% block extra_script %}
{%endblock extra_script %}
</head>
<body>
{% block container %}
<main role="main" class="container mt-4">
{% block content %} {% endblock content %}
</main>
{% endblock container %} {% block footer %} {% endblock footer %}
<!-- Bootstrap Javascript -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
{% block body_script %} {% endblock body_script %}
</body>
</html>

View File

@@ -1,64 +0,0 @@
{% extends "base.html" %} {% load static %} {% block content %}
<h1>List Mosquitto Users</h1>
<button class="btn btn-primary mb-3" hx-get="/add_user" hx-target="#content">
Add User
</button>
<table class="table table-striped">
<thead>
<tr>
<th>Site</th>
<th>Username</th>
<th>Client ID</th>
<th>Topic</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.site}}</td>
<td>
<a
href="/wallet/user/edit/{{user.id}}"
hx-get="/wallet/user/edit/{{user.id}}"
hx-target="#content"
>{{user.username}}</a
>
</td>
<td>{{user.client_id}}</td>
<td>{{user.topic}}</td>
<td>
<a
href="/wallet/user/role/{{user.role}}"
hx-get="/wallet/user/role/{{user.role}}"
hx-target="#content"
>{{user.role}}</a
>
</td>
<td>
{% if user.status == "enabled" %}
<button
class="btn btn-danger btn-sm"
hx-post="/wallet/user/disable/{{user.id}}"
hx-target="#user-list"
>
Disable
</button>
{% else %}
<button
class="btn btn-warning btn-sm"
hx-post="/wallet/user/enable/{{user.id}}"
hx-target="#user-list"
>
Enable
</button>
{% endif %}
</td>
</tr>
{% empty %}
<i> No users </i>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View File

@@ -2,13 +2,15 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('', views.home_view, name="home"),
path('add/', views.add_password_api, name='add_user'), path('add/', views.add_password_api, name='add_user'),
path('disable/', views.disable_password_api, name='disable_user'),
path('get/', views.get_password_api, name='get_password'), path('get/', views.get_password_api, name='get_password'),
path('list/', views.list_sites_api, name='list_sites'), path('list/', views.list_sites_api, name='list_sites'),
path('user/list/', views.list_users, name='list_users'),
path('user/add/', views.add_user, name='add_user'),
path('user/edit/<int:user_id>', views.edit_user, name='edit_user'),
path('user/role/<str:role>', views.view_role, name='view_role'),
path('publish', views.publish_message, name='publish'), path('publish', views.publish_message, name='publish'),
path('user/list/', views.list_users, name='list_users'),
path('user/create_user/', views.create_user, name='create_user'),
path('user/edit/<slug:slug>', views.edit_user, name='edit_user'),
path('user/disable/<slug:slug>', views.disable_user, name='disable_user'),
path('user/enable/<slug:slug>', views.enable_user, name='enable_user'),
path('user/role/<str:role>', views.view_role, name='view_role'),
] ]

View File

@@ -15,7 +15,7 @@ def save_master_hash(hash):
# Carica l'hash della master password # Carica l'hash della master password
def load_master_hash(): def load_master_hash():
try: try:
entry = MasterHash.objects.get(id="1") entry = MasterHash.objects.get()
return entry.hash return entry.hash
except Exception as e: except Exception as e:
return None return None

76
wallet_api/utils_new.py Normal file
View File

@@ -0,0 +1,76 @@
import bcrypt
import base64
import hashlib
import json
from cryptography.fernet import Fernet
from .models import MasterHash
from django.http import JsonResponse
# 1. Salva l'hash della master password e la chiave principale cifrata
def save_master_data(hashed_password, encrypted_data_key):
entry, created = MasterHash.objects.get_or_create(id="1")
entry.hash = hashed_password
entry.encrypted_data_key = encrypted_data_key
entry.save()
# 2. Carica i dati della master password
def load_master_data():
try:
entry = MasterHash.objects.get(id="1")
return entry.hash, entry.encrypted_data_key
except Exception:
return None, None
# 3. Autenticazione della master password
def authenticate(master_password):
stored_hash, encrypted_data_key = load_master_data()
if stored_hash is None:
hashed_password = bcrypt.hashpw(master_password.encode(), bcrypt.gensalt())
key = Fernet.generate_key() # Genera una chiave principale
derived_key = derive_key(master_password) # Deriva la chiave dalla master password
encrypted_data_key = encrypt_password(key.decode(), derived_key) # Cifra la chiave principale
save_master_data(hashed_password, encrypted_data_key)
return True, key
# Controlla se la password inserita è corretta
if bcrypt.checkpw(master_password.encode(), stored_hash.tobytes()):
derived_key = derive_key(master_password)
decrypted_data_key = decrypt_password(encrypted_data_key, derived_key) # Decifra la chiave principale
return True, decrypted_data_key.encode()
return False, None
# 4. Funzione per cambiare la master password
def change_master_password(old_password, new_password):
authenticated, data_key = authenticate(old_password)
if not authenticated:
return False # Fallisce se la vecchia password non è corretta
# Deriviamo la nuova chiave dalla nuova master password
new_derived_key = derive_key(new_password)
# Cifriamo la chiave principale con la nuova chiave derivata
new_encrypted_data_key = encrypt_password(data_key.decode(), new_derived_key)
# Creiamo il nuovo hash della nuova master password
new_hashed_password = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt())
# Aggiorniamo il database con i nuovi dati
save_master_data(new_hashed_password, new_encrypted_data_key)
return True # Cambio password riuscito
# 5. Deriva una chiave da una password
def derive_key(master_password):
hash = hashlib.sha256(master_password.encode()).digest()
return base64.urlsafe_b64encode(hash)
# 6. Cifra una password con una chiave
def encrypt_password(password, key):
cipher = Fernet(key)
return cipher.encrypt(password.encode()).decode()
# 7. Decifra una password con una chiave
def decrypt_password(encrypted_password, key):
cipher = Fernet(key)
return cipher.decrypt(encrypted_password.encode()).decode()

View File

@@ -1,15 +1,25 @@
from django.http import JsonResponse from django.shortcuts import render, redirect, get_object_or_404
from django.shortcuts import render from django.contrib import messages
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .models import PasswordEntry, MasterHash
from .utils import authenticate, derive_key, encrypt_password, decrypt_password
from dotenv import dotenv_values
import json import json
import threading
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import paho.mqtt.publish as publish import paho.mqtt.publish as publish
from dotenv import dotenv_values
from .models import PasswordEntry
from .utils import authenticate, derive_key, encrypt_password, decrypt_password
from .mqtt_service import MosquittoDynamicSecurity
config = dotenv_values(".env") config = dotenv_values(".env")
authenticate(config['MASTER_PASSWORD'])
def home_view(request):
return render(request, 'home.html')
@csrf_exempt @csrf_exempt
def publish_message(request): def publish_message(request):
@@ -25,34 +35,34 @@ def publish_message(request):
def list_users(request): def list_users(request):
users = PasswordEntry.objects.all() users = PasswordEntry.objects.all()
lock_users = threading.Lock()
return render(request, 'wallet_api/list_users.html', {'users': users}) return render(request, 'wallet_api/list_users.html', {'users': users})
def edit_user(request, user_id): def create_user(request):
user = PasswordEntry.objects.filter(id=user_id).first()
return render(request, 'wallet_api/edit_user.html', {'user': user})
def add_user(request):
return render(request, 'wallet_api/add_user.html')
def view_role(request, role):
return render(request, 'wallet_api/role_info.html', {'role': role })
@csrf_exempt
def disable_password_api(request):
if request.method == 'POST': if request.method == 'POST':
data = json.loads(request.body) site = request.POST.get('site')
master_password = data.get('master_password') username = request.POST.get('username')
site = data.get('site') client_id = request.POST.get('clientId')
username = data.get('username') topic = request.POST.get('topic')
password = data.get('password') password = request.POST.get('password')
client_id = data.get('client_id')
topic = data.get('topic')
if not authenticate(master_password): # Comando per creare un utente
return JsonResponse({"error": "Master password errata"}, status=403) command = {
"commands":
[
{
"command": "createClient",
"username": username,
"password": password
}
]
}
key = derive_key(master_password) # Invia il comando a Mosquitto
mqtt_service = MosquittoDynamicSecurity()
response = mqtt_service.send_command(command)
if "error" not in response["responses"][0]:
key = derive_key(config['MASTER_PASSWORD'])
encrypted_password = encrypt_password(password, key) encrypted_password = encrypt_password(password, key)
PasswordEntry.objects.create( PasswordEntry.objects.create(
@@ -60,9 +70,86 @@ def disable_password_api(request):
username=username, username=username,
password=encrypted_password, password=encrypted_password,
client_id=client_id, client_id=client_id,
topic=topic topic=topic,
status='enabled'
) )
return JsonResponse({"message": "Password aggiunta con successo"})
messages.success(request, 'Utente creato con successo!') # Messaggio di successo
return redirect('list_users') # Reindirizza alla lista degli utenti
else:
messages.warning(request, f'Errore durante la creazione dell\'utente: {response["responses"][0]["error"]}') # Messaggio di errore
return render(request, 'wallet_api/create_user.html', {'response': response}) # Rimani sulla stessa vista
# Se la richiesta non è POST, mostra il form di creazione utente
return render(request, 'wallet_api/create_user.html')
def edit_user(request, slug):
user = PasswordEntry.objects.filter(slug=slug).first()
return render(request, 'wallet_api/edit_user.html', {'user': user})
def disable_user(request, slug):
user = PasswordEntry.objects.filter(slug=slug).values('id','username')
print(user.first())
# Comando per creare un utente
command = {
"commands":
[
{
"command": "disableClient",
"username": user.first()["username"]
}
]
}
# Invia il comando a Mosquitto
mqtt_service = MosquittoDynamicSecurity()
response = mqtt_service.send_command(command)
if "error" not in response["responses"][0]:
PasswordEntry.objects.update(
status='disabled'
)
messages.success(request, 'Utente disabilitato con successo!') # Messaggio di successo
if request.htmx:
return render(request, 'partials/enable_user.html')
return redirect('list_users') # Reindirizza alla lista degli utenti
else:
messages.warning(request, f'Errore durante la disabilitazione dell\'utente: {response["responses"][0]["error"]}') # Messaggio di errore
return redirect('list_users') # Reindirizza alla lista degli utenti
def enable_user(request, slug):
user = PasswordEntry.objects.filter(slug=slug).values('id','username')
print(user.first())
# Comando per creare un utente
command = {
"commands":
[
{
"command": "enableClient",
"username": user.first()["username"]
}
]
}
# Invia il comando a Mosquitto
mqtt_service = MosquittoDynamicSecurity()
response = mqtt_service.send_command(command)
if "error" not in response["responses"][0]:
PasswordEntry.objects.update(
status='enabled'
)
messages.success(request, 'Utente abilitato con successo!') # Messaggio di successo
return redirect('list_users') # Reindirizza alla lista degli utenti
else:
messages.warning(request, f'Errore durante la abilitazione dell\'utente: {response["responses"][0]["error"]}') # Messaggio di errore
return redirect('list_users') # Reindirizza alla lista degli utenti
def view_role(request, role):
return render(request, 'wallet_api/role_info.html', {'role': role })
@csrf_exempt @csrf_exempt
def get_password_api(request): def get_password_api(request):
@@ -122,8 +209,6 @@ def add_password_api(request):
username=username, username=username,
password=encrypted_password, password=encrypted_password,
client_id=client_id, client_id=client_id,
topic=topic, topic=topic
role='',
acls='{}'
) )
return JsonResponse({"message": "Password aggiunta con successo"}) return JsonResponse({"message": "Password aggiunta con successo"})