This commit is contained in:
2025-01-11 00:53:16 +01:00
parent a5b6b54429
commit cc66878cf7
11 changed files with 304 additions and 1 deletions

View File

@@ -8,6 +8,8 @@ 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-htmx>=1.21.0",
"paho-mqtt>=2.1.0",
"psycopg2-binary>=2.9.10", "psycopg2-binary>=2.9.10",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
] ]

65
static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

26
uv.lock generated
View File

@@ -118,6 +118,19 @@ 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-htmx"
version = "1.21.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/5c/f6b7e9102a86c69b018142c5e3255b7f983b4f6545fe8e9bbb326e903be1/django_htmx-1.21.0.tar.gz", hash = "sha256:6ed3b42effd5980f22e68f36cd14ee4311bff3b6cb8435a89e27f45995691572", size = 9611 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/ec/0d68c022dac4a539cdd2cf8b2e23a034da9d6ef864e4bc324a5fe2b50c28/django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:64bc31463017a80552b767bc216ee5700248fa72e7ccd2963495e69afbdb6abe", size = 6901 },
]
[[package]] [[package]]
name = "django-wallet" name = "django-wallet"
version = "0.1.0" version = "0.1.0"
@@ -126,6 +139,8 @@ dependencies = [
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "django" }, { name = "django" },
{ name = "django-htmx" },
{ name = "paho-mqtt" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
] ]
@@ -135,10 +150,21 @@ 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-htmx", specifier = ">=1.21.0" },
{ name = "paho-mqtt", specifier = ">=2.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" },
] ]
[[package]]
name = "paho-mqtt"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 },
]
[[package]] [[package]]
name = "psycopg2-binary" name = "psycopg2-binary"
version = "2.9.10" version = "2.9.10"

View File

@@ -38,6 +38,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_htmx',
'wallet_api.apps.WalletApiConfig', 'wallet_api.apps.WalletApiConfig',
] ]
@@ -49,6 +50,7 @@ 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',
"django_htmx.middleware.HtmxMiddleware",
] ]
ROOT_URLCONF = 'wallet.urls' ROOT_URLCONF = 'wallet.urls'
@@ -123,6 +125,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / "static"]
# 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

View File

@@ -0,0 +1,130 @@
"""
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

@@ -1,3 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import PasswordEntry
# Register your models here. # Register your models here.
admin.site.register(PasswordEntry)

View File

@@ -13,6 +13,9 @@ class PasswordEntry(models.Model):
class Meta: class Meta:
unique_together = ('site', 'username', 'client_id') unique_together = ('site', 'username', 'client_id')
def __str__(self):
return self.username
class MasterHash(models.Model): class MasterHash(models.Model):
hash = models.BinaryField() hash = models.BinaryField()
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,39 @@
{% 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.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
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">
{% block content %} {% endblock content %}
</main>
{% endblock container %} {% block footer %} {% endblock footer %}
<!-- Bootstrap Javascript -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
{% block body_script %} {% endblock body_script %}
</body>
</html>

View File

@@ -0,0 +1,27 @@
{% extends "base.html" %} {% load static %} {% block content %}
<h1>List Mosquitto Users</h1>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Site</th>
<th scope="col">Username</th>
<th scope="col">Client ID</th>
<th scope="col">Topic</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<th scope="row">{{ user.id }}</th>
<td>{{ user.site }}</td>
<td>{{ user.username }}</td>
<td>{{ user.client_id }}</td>
<td>{{ user.topic }}</td>
</tr>
{% empty %}
<i> No users </i>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View File

@@ -6,4 +6,5 @@ urlpatterns = [
path('get/', views.get_password_api, name='get_password'), path('get/', views.get_password_api, name='get_password'),
path('delete/', views.delete_password_api, name='delete_password'), path('delete/', views.delete_password_api, name='delete_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'),
] ]

View File

@@ -1,9 +1,14 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .models import PasswordEntry from .models import PasswordEntry, MasterHash
from .utils import authenticate, derive_key, encrypt_password, decrypt_password from .utils import authenticate, derive_key, encrypt_password, decrypt_password
import json import json
def list_users(request):
users = PasswordEntry.objects.all()
return render(request, 'wallet_api/list_users.html', {'users': users})
@csrf_exempt @csrf_exempt
def add_password_api(request): def add_password_api(request):
if request.method == 'POST': if request.method == 'POST':