Compare commits
1 commit
main
...
profile-ge
Author | SHA1 | Date | |
---|---|---|---|
639ea92368 |
102 changed files with 283 additions and 4011 deletions
|
@ -1 +0,0 @@
|
||||||
localhost
|
|
|
@ -1 +0,0 @@
|
||||||
Délégation Générale Numérique <dgsi@localhost>
|
|
|
@ -1 +0,0 @@
|
||||||
https://sso.dgnum.eu
|
|
|
@ -1 +0,0 @@
|
||||||
dgsi@localhost
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,11 +2,9 @@
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3.*
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
.static/*
|
.static/*
|
||||||
!.static/.gitkeep
|
!.static/.gitkeep
|
||||||
src/shared/static/bulma/bulma.css
|
src/shared/static/bulma/bulma.css
|
||||||
src/shared/static/bulma/bulma.css.map
|
src/shared/static/bulma/bulma.css.map
|
||||||
.credentials/KANIDM_AUTH_TOKEN
|
|
||||||
|
|
76
default.nix
76
default.nix
|
@ -8,44 +8,22 @@ let
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
hooks = {
|
hooks = {
|
||||||
|
# JS hooks
|
||||||
|
eslint.enable = true;
|
||||||
|
|
||||||
# Python hooks
|
# Python hooks
|
||||||
black = {
|
ruff.enable = true;
|
||||||
enable = true;
|
black.enable = true;
|
||||||
stages = [ "pre-push" ];
|
isort.enable = true;
|
||||||
};
|
|
||||||
|
|
||||||
isort = {
|
|
||||||
enable = true;
|
|
||||||
stages = [ "pre-push" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
ruff = {
|
|
||||||
enable = true;
|
|
||||||
stages = [ "pre-push" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Nix Hooks
|
# Nix Hooks
|
||||||
statix = {
|
statix.enable = true;
|
||||||
enable = true;
|
deadnix.enable = true;
|
||||||
stages = [ "pre-push" ];
|
|
||||||
};
|
|
||||||
deadnix = {
|
|
||||||
enable = true;
|
|
||||||
stages = [ "pre-push" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Misc Hooks
|
# Misc Hooks
|
||||||
commitizen.enable = true;
|
commitizen.enable = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
python = pkgs.python312.override {
|
|
||||||
packageOverrides =
|
|
||||||
self: _:
|
|
||||||
pkgs.lib.genAttrs (builtins.attrNames (builtins.readDir ./pkgs)) (
|
|
||||||
p: self.callPackage ./pkgs/${p} { }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -53,42 +31,28 @@ in
|
||||||
name = "dgsi.dev";
|
name = "dgsi.dev";
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
pkgs.dart-sass
|
|
||||||
pkgs.gettext
|
|
||||||
pkgs.jq
|
pkgs.jq
|
||||||
|
pkgs.dart-sass
|
||||||
|
|
||||||
# Python dependencies
|
# Python dependencies
|
||||||
(python.withPackages (
|
(pkgs.python3.withPackages (
|
||||||
ps:
|
ps:
|
||||||
[
|
[
|
||||||
|
ps.daphne
|
||||||
ps.django
|
ps.django
|
||||||
ps.django-allauth
|
ps.django-allauth
|
||||||
ps.django-allauth-cas
|
|
||||||
ps.django-browser-reload
|
|
||||||
ps.django-bulma-forms
|
|
||||||
ps.django-compressor
|
ps.django-compressor
|
||||||
ps.django-debug-toolbar
|
ps.django-debug-toolbar
|
||||||
ps.django-htmx
|
ps.django-types
|
||||||
ps.django-import-export
|
|
||||||
ps.django-sass-processor
|
|
||||||
ps.django-sass-processor-dart-sass
|
|
||||||
ps.django-sesame
|
|
||||||
ps.django-stubs
|
|
||||||
ps.django-unfold
|
|
||||||
ps.djangorestframework
|
|
||||||
ps.drf-spectacular
|
|
||||||
ps.ipython
|
|
||||||
ps.loadcredential
|
ps.loadcredential
|
||||||
ps.pykanidm
|
|
||||||
ps.python-cas
|
|
||||||
ps.django-extensions
|
|
||||||
ps.werkzeug
|
|
||||||
ps.pyopenssl
|
|
||||||
]
|
]
|
||||||
++ ps.django-allauth.optional-dependencies.saml
|
++ (builtins.map (p: ps.callPackage ./pkgs/${p} { }) [
|
||||||
++ ps.django-allauth.optional-dependencies.socialaccount
|
"django-browser-reload"
|
||||||
++ ps.django-sesame.optional-dependencies.ua
|
"django-bulma-forms"
|
||||||
++ ps.drf-spectacular.optional-dependencies.sidecar
|
"django-sass-processor"
|
||||||
|
"django-sass-processor-dart-sass"
|
||||||
|
"pykanidm"
|
||||||
|
])
|
||||||
))
|
))
|
||||||
] ++ check.enabledPackages;
|
] ++ check.enabledPackages;
|
||||||
|
|
||||||
|
@ -96,9 +60,7 @@ in
|
||||||
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
|
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
|
||||||
DGSI_DEBUG = "true";
|
DGSI_DEBUG = "true";
|
||||||
DGSI_STATIC_ROOT = builtins.toString ./.static;
|
DGSI_STATIC_ROOT = builtins.toString ./.static;
|
||||||
DGSI_MEDIA_ROOT = builtins.toString ./.media;
|
|
||||||
DGSI_KANIDM_CLIENT = "dgsi_test";
|
DGSI_KANIDM_CLIENT = "dgsi_test";
|
||||||
DGSI_ARCHIVES_ROOT = builtins.toString ./.archives;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
@ -8,15 +8,15 @@
|
||||||
"repo": "git-hooks.nix"
|
"repo": "git-hooks.nix"
|
||||||
},
|
},
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"revision": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
|
"revision": "e35aed5fda3cc79f88ed7f1795021e559582093a",
|
||||||
"url": "https://github.com/cachix/git-hooks.nix/archive/9364dc02281ce2d37a1f55b6e51f7c0f65a75f17.tar.gz",
|
"url": "https://github.com/cachix/pre-commit-hooks.nix/archive/e35aed5fda3cc79f88ed7f1795021e559582093a.tar.gz",
|
||||||
"hash": "1n2qlj5l8c4g7gm5v6rvc4hff3ka8ljv7y62inybli093bd2ypa7"
|
"hash": "1bq0yrjmkddj964s2q6393nwp4mqrlmc2i5wsy992r034awyywp1"
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"type": "Channel",
|
"type": "Channel",
|
||||||
"name": "nixpkgs-unstable",
|
"name": "nixpkgs-unstable",
|
||||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre776128.eb0e0f21f15c/nixexprs.tar.xz",
|
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre630522.3305b2b25e4a/nixexprs.tar.xz",
|
||||||
"hash": "0l04lkdi3slwwlgwyr8x0argzxcxm16a4hkijfxbjhlj44y1bkif"
|
"hash": "1bg240s2jbyvdixpy14rc4fcn9zrjf36mcd2xv59rcxx508gwhi2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": 3
|
"version": 3
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
diff --git a/setup.py b/setup.py
|
|
||||||
index fb06ec0..506677f 100644
|
|
||||||
--- a/setup.py
|
|
||||||
+++ b/setup.py
|
|
||||||
@@ -52,7 +52,6 @@ setup(
|
|
||||||
install_requires=[
|
|
||||||
"django-allauth",
|
|
||||||
"python-cas",
|
|
||||||
- "six",
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
"docs": ["sphinx"],
|
|
|
@ -1,39 +0,0 @@
|
||||||
diff --git a/allauth_cas/signals.py b/allauth_cas/signals.py
|
|
||||||
index 36c9b24..530c26e 100644
|
|
||||||
--- a/allauth_cas/signals.py
|
|
||||||
+++ b/allauth_cas/signals.py
|
|
||||||
@@ -1,4 +1,4 @@
|
|
||||||
-from allauth.account.adapter import get_adapter
|
|
||||||
+from allauth.socialaccount.adapter import get_adapter
|
|
||||||
from allauth.account.utils import get_next_redirect_url
|
|
||||||
from allauth.socialaccount import providers
|
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
@@ -14,7 +14,7 @@ def cas_account_logout(sender, request, **kwargs):
|
|
||||||
if not provider_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
- provider = providers.registry.by_id(provider_id, request)
|
|
||||||
+ provider = get_adapter(request).get_provider(request, provider_id)
|
|
||||||
|
|
||||||
if not provider.message_suggest_caslogout_on_logout(request):
|
|
||||||
return
|
|
||||||
diff --git a/allauth_cas/views.py b/allauth_cas/views.py
|
|
||||||
index d08e354..9e81e53 100644
|
|
||||||
--- a/allauth_cas/views.py
|
|
||||||
+++ b/allauth_cas/views.py
|
|
||||||
@@ -1,5 +1,5 @@
|
|
||||||
import cas
|
|
||||||
-from allauth.account.adapter import get_adapter
|
|
||||||
+from allauth.socialaccount.adapter import get_adapter
|
|
||||||
from allauth.account.utils import get_next_redirect_url
|
|
||||||
from allauth.socialaccount import providers
|
|
||||||
from allauth.socialaccount.helpers import (
|
|
||||||
@@ -56,7 +56,7 @@ class CASAdapter:
|
|
||||||
"""
|
|
||||||
Returns a provider instance for the current request.
|
|
||||||
"""
|
|
||||||
- return providers.registry.by_id(self.provider_id, self.request)
|
|
||||||
+ return get_adapter(self.request).get_provider(self.request, self.provider_id)
|
|
||||||
|
|
||||||
def complete_login(self, request, response):
|
|
||||||
"""
|
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchFromGitHub,
|
|
||||||
setuptools,
|
|
||||||
wheel,
|
|
||||||
django-allauth,
|
|
||||||
python-cas,
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "django-allauth-cas";
|
|
||||||
version = "unstable-2024-01-25";
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "jlucasp25";
|
|
||||||
repo = "django-allauth-cas";
|
|
||||||
rev = "77e02f3796cd564a9a0c48b5b568b14d4d4c5687";
|
|
||||||
hash = "sha256-y/IquXl/4+9MJmsgbWtPun3tBbRJ4kJFzWo5c+5WeHk=";
|
|
||||||
};
|
|
||||||
|
|
||||||
patches = [
|
|
||||||
./01-setup.patch
|
|
||||||
./02-registry.patch
|
|
||||||
];
|
|
||||||
|
|
||||||
build-system = [
|
|
||||||
setuptools
|
|
||||||
wheel
|
|
||||||
];
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
django-allauth
|
|
||||||
python-cas
|
|
||||||
];
|
|
||||||
|
|
||||||
pythonImportsCheck = [
|
|
||||||
"allauth_cas"
|
|
||||||
];
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "CAS support for django-allauth";
|
|
||||||
homepage = "https://github.com/jlucasp25/django-allauth-cas";
|
|
||||||
changelog = "https://github.com/jlucasp25/django-allauth-cas/blob/${src.rev}/CHANGELOG.rst";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
maintainers = with lib.maintainers; [ ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchFromGitHub,
|
|
||||||
python,
|
|
||||||
poetry-core,
|
|
||||||
django,
|
|
||||||
ua-parser,
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "django-sesame";
|
|
||||||
version = "3.2.3";
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "aaugustin";
|
|
||||||
repo = "django-sesame";
|
|
||||||
rev = version;
|
|
||||||
hash = "sha256-JpbmcV5hAZkW15cizsAJhmTda4xtML0EY/PJdVSInUs=";
|
|
||||||
};
|
|
||||||
|
|
||||||
build-system = [
|
|
||||||
poetry-core
|
|
||||||
];
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
django
|
|
||||||
];
|
|
||||||
|
|
||||||
optional-dependencies = {
|
|
||||||
ua = [
|
|
||||||
ua-parser
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
pythonImportsCheck = [
|
|
||||||
"sesame"
|
|
||||||
];
|
|
||||||
|
|
||||||
checkPhase = ''
|
|
||||||
runHook preCheck
|
|
||||||
|
|
||||||
${python.interpreter} -m django test --settings=tests.settings
|
|
||||||
|
|
||||||
runHook postCheck
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "URLs with authentication tokens for one-click login";
|
|
||||||
homepage = "https://github.com/aaugustin/django-sesame";
|
|
||||||
license = lib.licenses.bsd3;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchFromGitHub,
|
|
||||||
poetry-core,
|
|
||||||
django,
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "django-unfold";
|
|
||||||
version = "0.39.0";
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "unfoldadmin";
|
|
||||||
repo = "django-unfold";
|
|
||||||
rev = version;
|
|
||||||
hash = "sha256-CmmlTx2eLcANc6ANy25ii1KVebkmUEJmDCe+/RwakAg=";
|
|
||||||
};
|
|
||||||
|
|
||||||
build-system = [
|
|
||||||
poetry-core
|
|
||||||
];
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
django
|
|
||||||
];
|
|
||||||
|
|
||||||
pythonImportsCheck = [
|
|
||||||
"unfold"
|
|
||||||
];
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "Modern Django admin theme for seamless interface development";
|
|
||||||
homepage = "https://github.com/unfoldadmin/django-unfold";
|
|
||||||
changelog = "https://github.com/unfoldadmin/django-unfold/blob/${src.rev}/CHANGELOG.md";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
maintainers = with lib.maintainers; [ ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchFromGitHub,
|
|
||||||
setuptools,
|
|
||||||
wheel,
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "loadcredential";
|
|
||||||
version = "1.2";
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "Tom-Hubrecht";
|
|
||||||
repo = "loadcredential";
|
|
||||||
rev = "v${version}";
|
|
||||||
hash = "sha256-rNWFD89h1p1jYWLcfzsa/w8nK3bR4aVJsUPx0UtZnIw=";
|
|
||||||
};
|
|
||||||
|
|
||||||
build-system = [
|
|
||||||
setuptools
|
|
||||||
wheel
|
|
||||||
];
|
|
||||||
|
|
||||||
pythonImportsCheck = [ "loadcredential" ];
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "A simple python package to read credentials passed through systemd's LoadCredential, with a fallback on env variables ";
|
|
||||||
homepage = "https://github.com/Tom-Hubrecht/loadcredential";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
maintainers = [ ]; # with lib.maintainers; [ thubrecht ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -11,14 +11,14 @@
|
||||||
|
|
||||||
buildPythonPackage rec {
|
buildPythonPackage rec {
|
||||||
pname = "kanidm";
|
pname = "kanidm";
|
||||||
version = "1.3.3";
|
version = "1.1.0-rc.16";
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
src = fetchFromGitHub {
|
||||||
owner = "kanidm";
|
owner = "kanidm";
|
||||||
repo = "kanidm";
|
repo = "kanidm";
|
||||||
rev = "v${version}";
|
rev = "v${version}";
|
||||||
hash = "sha256-W5G7osV4du6w/BfyY9YrDzorcLNizRsoz70RMfO2AbY=";
|
hash = "sha256-NH9V5KKI9LAtJ2/WuWtUJUzkjVMfO7Q5NQkK7Ys2olU=";
|
||||||
};
|
};
|
||||||
|
|
||||||
sourceRoot = "source/pykanidm";
|
sourceRoot = "source/pykanidm";
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
buildPythonPackage,
|
|
||||||
fetchFromGitHub,
|
|
||||||
setuptools,
|
|
||||||
wheel,
|
|
||||||
lxml,
|
|
||||||
requests,
|
|
||||||
six,
|
|
||||||
}:
|
|
||||||
|
|
||||||
buildPythonPackage rec {
|
|
||||||
pname = "python-cas";
|
|
||||||
version = "1.6.0";
|
|
||||||
pyproject = true;
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "python-cas";
|
|
||||||
repo = "python-cas";
|
|
||||||
rev = "v${version}";
|
|
||||||
hash = "sha256-0lpjG/Sma0tJGtahiFE1CjvTyswrBUp+F6f1S65b+lk=";
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
setuptools
|
|
||||||
wheel
|
|
||||||
];
|
|
||||||
|
|
||||||
propagatedBuildInputs = [
|
|
||||||
lxml
|
|
||||||
requests
|
|
||||||
six
|
|
||||||
];
|
|
||||||
|
|
||||||
pythonImportsCheck = [ "cas" ];
|
|
||||||
|
|
||||||
meta = with lib; {
|
|
||||||
description = "Python CAS (Central Authentication Service) client library support CAS 1.0/2.0/3.0";
|
|
||||||
homepage = "https://github.com/python-cas/python-cas";
|
|
||||||
license = licenses.mit;
|
|
||||||
maintainers = with maintainers; [ ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -21,7 +21,6 @@ Repository = "https://git.dgnum.eu/DGNum/dgsi"
|
||||||
|
|
||||||
[tool.djlint]
|
[tool.djlint]
|
||||||
blank_line_after_tag = "load,extends"
|
blank_line_after_tag = "load,extends"
|
||||||
custom_blocks = "slot,element"
|
|
||||||
format_js = true
|
format_js = true
|
||||||
indent = 2
|
indent = 2
|
||||||
max_blank_lines = 1
|
max_blank_lines = 1
|
||||||
|
@ -29,6 +28,3 @@ profile = "django"
|
||||||
|
|
||||||
[tool.djlint.js]
|
[tool.djlint.js]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from api.models import ServiceUser, Token
|
|
||||||
from shared.admin import BaseModelAdmin
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ServiceUser)
|
|
||||||
class UserAdmin(BaseModelAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"username",
|
|
||||||
"email",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(_("Permissions"), {"fields": ("groups", "user_permissions")}),
|
|
||||||
)
|
|
||||||
|
|
||||||
add_fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"classes": ("wide",),
|
|
||||||
"fields": ("username",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
filter_horizontal = (
|
|
||||||
"groups",
|
|
||||||
"user_permissions",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Token)
|
|
||||||
class AdminClass(BaseModelAdmin):
|
|
||||||
readonly_fields = ("key",)
|
|
||||||
|
|
||||||
add_fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"classes": ("wide",),
|
|
||||||
"fields": ("user",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "api"
|
|
|
@ -1,66 +0,0 @@
|
||||||
from django.contrib.auth.backends import BaseBackend
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from rest_framework.authentication import TokenAuthentication as Authentication
|
|
||||||
|
|
||||||
from api.models import ServiceUser, Token
|
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthentication(Authentication):
|
|
||||||
model = Token
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceUserBackend(BaseBackend):
|
|
||||||
"""
|
|
||||||
This backend is only used to fetch permissions of a ServiceUser.
|
|
||||||
For this reason, the authenticate part is left out.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _get_user_permissions(self, user_obj):
|
|
||||||
return user_obj.user_permissions.all()
|
|
||||||
|
|
||||||
def _get_group_permissions(self, user_obj):
|
|
||||||
return Permission.objects.filter(group__serviceuser=user_obj)
|
|
||||||
|
|
||||||
def _get_permissions(self, user_obj, obj, from_name):
|
|
||||||
"""
|
|
||||||
Return the permissions of `user_obj` from `from_name`. `from_name` can
|
|
||||||
be either "group" or "user" to return permissions from
|
|
||||||
`_get_group_permissions` or `_get_user_permissions` respectively.
|
|
||||||
"""
|
|
||||||
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
perm_cache_name = "_%s_perm_cache" % from_name
|
|
||||||
if not hasattr(user_obj, perm_cache_name):
|
|
||||||
if user_obj.is_superuser:
|
|
||||||
perms = Permission.objects.all()
|
|
||||||
else:
|
|
||||||
perms = getattr(self, "_get_%s_permissions" % from_name)(user_obj)
|
|
||||||
perms = perms.values_list("content_type__app_label", "codename").order_by()
|
|
||||||
setattr(
|
|
||||||
user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms}
|
|
||||||
)
|
|
||||||
return getattr(user_obj, perm_cache_name)
|
|
||||||
|
|
||||||
def get_user_permissions(self, user_obj, obj=None):
|
|
||||||
"""
|
|
||||||
Return a set of permission strings the user `user_obj` has from their
|
|
||||||
`user_permissions`.
|
|
||||||
"""
|
|
||||||
return self._get_permissions(user_obj, obj, "user")
|
|
||||||
|
|
||||||
def get_group_permissions(self, user_obj, obj=None):
|
|
||||||
"""
|
|
||||||
Return a set of permission strings the user `user_obj` has from the
|
|
||||||
groups they belong.
|
|
||||||
"""
|
|
||||||
return self._get_permissions(user_obj, obj, "group")
|
|
||||||
|
|
||||||
def get_all_permissions(self, user_obj, obj=None):
|
|
||||||
if not isinstance(user_obj, ServiceUser) or obj is not None:
|
|
||||||
return set()
|
|
||||||
if not hasattr(user_obj, "_perm_cache"):
|
|
||||||
setattr(user_obj, "_perm_cache", super().get_all_permissions(user_obj))
|
|
||||||
|
|
||||||
# NOTE: We just set the perm cache, so it has to exist, no matter what the typing system tells us
|
|
||||||
return user_obj._perm_cache # type: ignore
|
|
|
@ -1,134 +0,0 @@
|
||||||
# Generated by Django 4.2.17 on 2025-03-30 21:29
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
import rest_framework.authtoken.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import api.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("auth", "0012_alter_user_first_name_max_length"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="ServiceUser",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"last_login",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True, null=True, verbose_name="last login"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"username",
|
|
||||||
models.CharField(
|
|
||||||
error_messages={
|
|
||||||
"unique": "A user with that username already exists."
|
|
||||||
},
|
|
||||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
|
||||||
max_length=150,
|
|
||||||
unique=True,
|
|
||||||
validators=[
|
|
||||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
|
||||||
],
|
|
||||||
verbose_name="username",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"email",
|
|
||||||
models.EmailField(
|
|
||||||
blank=True, max_length=254, verbose_name="email address"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"date_joined",
|
|
||||||
models.DateTimeField(
|
|
||||||
default=django.utils.timezone.now, verbose_name="date joined"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"password",
|
|
||||||
models.CharField(
|
|
||||||
default=api.models.unusable_password,
|
|
||||||
editable=False,
|
|
||||||
max_length=128,
|
|
||||||
verbose_name="password",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"groups",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
|
||||||
to="auth.group",
|
|
||||||
verbose_name="groups",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user_permissions",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Specific permissions for this user.",
|
|
||||||
to="auth.permission",
|
|
||||||
verbose_name="user permissions",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Service user",
|
|
||||||
"verbose_name_plural": "Service users",
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
("objects", django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Token",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="Created"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"key",
|
|
||||||
models.CharField(
|
|
||||||
default=rest_framework.authtoken.models.Token.generate_key,
|
|
||||||
max_length=40,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="Key",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="api.serviceuser",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Token",
|
|
||||||
"verbose_name_plural": "Tokens",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,65 +0,0 @@
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
from django.contrib.auth.models import AbstractUser, Group, Permission
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework.authtoken.models import Token as BaseToken
|
|
||||||
|
|
||||||
|
|
||||||
def unusable_password() -> str:
|
|
||||||
return make_password(None)
|
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# NOTE: We subclass the classic User model to create scoped authentication
|
|
||||||
# that is limited to the API usage.
|
|
||||||
class ServiceUser(AbstractUser):
|
|
||||||
"""
|
|
||||||
Service Users
|
|
||||||
"""
|
|
||||||
|
|
||||||
is_superuser = False
|
|
||||||
is_staff = False
|
|
||||||
is_active = True
|
|
||||||
|
|
||||||
password = models.CharField(
|
|
||||||
max_length=128,
|
|
||||||
verbose_name="password",
|
|
||||||
default=unusable_password,
|
|
||||||
editable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def first_name(self):
|
|
||||||
return self.username
|
|
||||||
|
|
||||||
last_name = "[service]"
|
|
||||||
|
|
||||||
groups = models.ManyToManyField(
|
|
||||||
Group,
|
|
||||||
verbose_name=_("groups"),
|
|
||||||
blank=True,
|
|
||||||
help_text=_(
|
|
||||||
"The groups this user belongs to. A user will get all permissions "
|
|
||||||
"granted to each of their groups."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
user_permissions = models.ManyToManyField(
|
|
||||||
Permission,
|
|
||||||
verbose_name=_("user permissions"),
|
|
||||||
blank=True,
|
|
||||||
help_text=_("Specific permissions for this user."),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Service user")
|
|
||||||
verbose_name_plural = _("Service users")
|
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseToken):
|
|
||||||
user = models.ForeignKey(ServiceUser, on_delete=models.CASCADE)
|
|
||||||
key = models.CharField(
|
|
||||||
_("Key"),
|
|
||||||
max_length=40,
|
|
||||||
primary_key=True,
|
|
||||||
default=BaseToken.generate_key,
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
from rest_framework.permissions import DjangoModelPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class DgsiModelPermissions(DjangoModelPermissions):
|
|
||||||
perms_map = {
|
|
||||||
"GET": ["%(app_label)s.view_%(model_name)s"],
|
|
||||||
"OPTIONS": [],
|
|
||||||
"HEAD": [],
|
|
||||||
"POST": ["%(app_label)s.add_%(model_name)s"],
|
|
||||||
"PUT": ["%(app_label)s.change_%(model_name)s"],
|
|
||||||
"PATCH": ["%(app_label)s.change_%(model_name)s"],
|
|
||||||
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from dgsi.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class DgsiUserSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["username", "email", "vlan_id"]
|
|
|
@ -1,17 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = "api"
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("user/<str:username>/", views.UserView.as_view()),
|
|
||||||
# Schema views
|
|
||||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
|
||||||
path(
|
|
||||||
"schema/swagger-ui/",
|
|
||||||
SpectacularSwaggerView.as_view(url_name="api:schema"),
|
|
||||||
name="swagger-ui",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,21 +0,0 @@
|
||||||
from django.http import HttpResponse, JsonResponse
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from api.permissions import DgsiModelPermissions
|
|
||||||
from api.serializers import DgsiUserSerializer
|
|
||||||
from dgsi.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserView(APIView):
|
|
||||||
permission_classes = [DgsiModelPermissions]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return User.objects.all()
|
|
||||||
|
|
||||||
def get(self, request, username, format=None):
|
|
||||||
try:
|
|
||||||
user = self.get_queryset().get(username=username)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
return HttpResponse(status=404)
|
|
||||||
|
|
||||||
return JsonResponse(DgsiUserSerializer(user).data)
|
|
|
@ -4,8 +4,6 @@ Django settings for the DGSI project.
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.contrib.messages import constants as messages
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from loadcredential import Credentials
|
from loadcredential import Credentials
|
||||||
|
|
||||||
credentials = Credentials(env_prefix="DGSI_")
|
credentials = Credentials(env_prefix="DGSI_")
|
||||||
|
@ -22,43 +20,26 @@ DEBUG = credentials.get_json("DEBUG", False)
|
||||||
|
|
||||||
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
|
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
|
||||||
|
|
||||||
ADMINS = credentials.get_json("ADMINS", [])
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# List the installed applications
|
# List the installed applications
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# Unfold apps
|
"daphne",
|
||||||
"unfold",
|
|
||||||
"unfold.contrib.import_export",
|
|
||||||
# Django standard apps
|
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
# Custom apps
|
|
||||||
"shared.staticfiles.StaticFilesApp", # Overrides the default staticfiles app to filter out the sccs sources
|
"shared.staticfiles.StaticFilesApp", # Overrides the default staticfiles app to filter out the sccs sources
|
||||||
"django_browser_reload",
|
|
||||||
"sass_processor",
|
"sass_processor",
|
||||||
"bulma",
|
"bulma",
|
||||||
"import_export",
|
|
||||||
"django_htmx",
|
|
||||||
# Authentication
|
# Authentication
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
"allauth.socialaccount",
|
"allauth.socialaccount",
|
||||||
"allauth.socialaccount.providers.openid_connect",
|
"allauth.socialaccount.providers.openid_connect",
|
||||||
# "allauth.socialaccount.providers.saml",
|
|
||||||
"allauth_cas",
|
|
||||||
"shared.cas",
|
|
||||||
# Main app
|
# Main app
|
||||||
"dgsi",
|
"dgsi",
|
||||||
# API
|
|
||||||
"rest_framework",
|
|
||||||
"api",
|
|
||||||
"drf_spectacular",
|
|
||||||
"drf_spectacular_sidecar",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -67,34 +48,14 @@ INSTALLED_APPS = [
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"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",
|
|
||||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
|
||||||
"allauth.account.middleware.AccountMiddleware",
|
"allauth.account.middleware.AccountMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
###
|
|
||||||
# Logging configuration
|
|
||||||
|
|
||||||
LOGGING = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"handlers": {
|
|
||||||
"console": {
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"handlers": ["console"],
|
|
||||||
"level": credentials.get("LOG_LEVEL", "WARNING"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# The main url configuration
|
# The main url configuration
|
||||||
|
|
||||||
|
@ -125,34 +86,20 @@ TEMPLATES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
###
|
###
|
||||||
# WSGI application configuration
|
# ASGI application configuration
|
||||||
|
|
||||||
WSGI_APPLICATION = "app.wsgi.application"
|
ASGI_APPLICATION = "app.asgi.application"
|
||||||
|
|
||||||
###
|
|
||||||
# E-Mail configuration
|
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL = credentials["FROM_EMAIL"]
|
|
||||||
EMAIL_HOST = credentials.get("EMAIL_HOST", "localhost")
|
|
||||||
EMAIL_HOST_PASSWORD = credentials.get("EMAIL_HOST_PASSWORD", "")
|
|
||||||
EMAIL_HOST_USER = credentials.get("EMAIL_HOST_USER", "")
|
|
||||||
EMAIL_PORT = credentials.get_json("EMAIL_PORT", 465)
|
|
||||||
EMAIL_USE_SSL = credentials.get("EMAIL_USE_SSL", False)
|
|
||||||
SERVER_EMAIL = credentials["SERVER_EMAIL"]
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Database configuration
|
# Database configuration
|
||||||
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = credentials.get_json(
|
DATABASES = {
|
||||||
"DATABASES",
|
|
||||||
{
|
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -160,14 +107,10 @@ DATABASES = credentials.get_json(
|
||||||
# Disable password validation, no authentication should use local passwords
|
# Disable password validation, no authentication should use local passwords
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
# NOTE: The ServiceUserBackend has to be first because of crimes committed:
|
|
||||||
# We create a second model of Users, unrelated to the auth_user_model,
|
|
||||||
# this goes against all assumptions made in Django's code
|
|
||||||
"api.authentication.ServiceUserBackend",
|
|
||||||
"allauth.account.auth_backends.AuthenticationBackend",
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
"sesame.backends.ModelBackend",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SOCIALACCOUNT_ONLY = True
|
||||||
SOCIALACCOUNT_PROVIDERS = {
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
"openid_connect": {
|
"openid_connect": {
|
||||||
"OAUTH_PKCE_ENABLED": True,
|
"OAUTH_PKCE_ENABLED": True,
|
||||||
|
@ -179,123 +122,22 @@ SOCIALACCOUNT_PROVIDERS = {
|
||||||
"secret": credentials["KANIDM_SECRET"],
|
"secret": credentials["KANIDM_SECRET"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"server_url": f"https://sso.dgnum.eu/oauth2/openid/{credentials['KANIDM_CLIENT']}",
|
"server_url": f"https://sso.dgnum.eu/oauth2/openid/{credentials['KANIDM_CLIENT']}",
|
||||||
"color": "primary",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"cas": {
|
|
||||||
"APP": {
|
|
||||||
"provider_id": "ens_cas",
|
|
||||||
"name": "CAS ENS",
|
|
||||||
"settings": {"color": "danger"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
# "saml": {
|
|
||||||
# "APPS": [
|
|
||||||
# {
|
|
||||||
# "provider_id": "ens_saml",
|
|
||||||
# "name": "SSO ENS",
|
|
||||||
# "client_id": "ens",
|
|
||||||
# "settings": {
|
|
||||||
# "color": "info",
|
|
||||||
# "idp": {
|
|
||||||
# "entity_id": "https://federation-test.ens.psl.eu/idp/shibboleth",
|
|
||||||
# "metadata_url": "https://federation-test.ens.psl.eu/idp/shibboleth",
|
|
||||||
# },
|
|
||||||
# # Our configuration
|
|
||||||
# "sp": {
|
|
||||||
# "entity_id": "https://profil.dgnum.eu/accounts/saml/ens/metadata",
|
|
||||||
# },
|
|
||||||
# "advanced": {
|
|
||||||
# "authn_request_signed": True,
|
|
||||||
# "metadata_signed": True,
|
|
||||||
# "private_key": credentials["X509_KEY"],
|
|
||||||
# "x509cert": credentials["X509_CERT"],
|
|
||||||
# "want_assertion_encrypted": False,
|
|
||||||
# "want_attribute_statement": True,
|
|
||||||
# "want_name_id": True,
|
|
||||||
# },
|
|
||||||
# "organization": {
|
|
||||||
# "en": {
|
|
||||||
# "name": "Délégation Générale Numérique",
|
|
||||||
# "displayname": "Délégation Générale Numérique",
|
|
||||||
# "url": "https://dgnum.eu",
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# "contact_person": {
|
|
||||||
# "technical": {
|
|
||||||
# "givenName": "Tom Hubrecht",
|
|
||||||
# "emailAddress": "admins@dgnum.eu",
|
|
||||||
# },
|
|
||||||
# "administrative": {
|
|
||||||
# "givenName": "Jean-Marc Gailis",
|
|
||||||
# "emailAddress": "bureau@dgnum.eu",
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# }
|
|
||||||
# ],
|
|
||||||
# },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SOCIALACCOUNT_ONLY = True
|
|
||||||
SOCIALACCOUNT_ADAPTER = "shared.account.SharedAccountAdapter"
|
|
||||||
ACCOUNT_EMAIL_VERIFICATION = "none"
|
|
||||||
ACCOUNT_AUTHENTICATION_METHOD = "username"
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = []
|
AUTH_PASSWORD_VALIDATORS = []
|
||||||
AUTH_USER_MODEL = "dgsi.User"
|
|
||||||
|
|
||||||
DGSI_STAFF_GROUP = credentials.get("STAFF_GROUP", "dgnum_bureau@sso.dgnum.eu")
|
|
||||||
DGSI_SUPERUSER_GROUP = credentials.get("SUPERUSER_GROUP", "dgnum_admins@sso.dgnum.eu")
|
|
||||||
|
|
||||||
VLAN_ID_MAX = 4094
|
|
||||||
VLAN_ID_MIN = (VLAN_ID_MAX - 850) + 1
|
|
||||||
VLAN_AUTOCONNECT = credentials.get("VLAN_AUTOCONNECT", False)
|
|
||||||
|
|
||||||
SESAME_MAX_AGE = 900
|
|
||||||
SESAME_ONE_TIME = True
|
|
||||||
SESAME_SIGNATURE_SIZE = 24
|
|
||||||
SESAME_TOKENS = ["sesame.tokens_v2"]
|
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# Rest Framework configuration
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
||||||
"api.authentication.TokenAuthentication",
|
|
||||||
],
|
|
||||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
|
||||||
"TITLE": "DG·SI API",
|
|
||||||
"VERSION": "0.1.0",
|
|
||||||
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
|
||||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
|
||||||
"REDOC_DIST": "SIDECAR",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Internationalization configuration
|
# Internationalization configuration
|
||||||
# -> https://docs.djangoproject.com/en/4.2/topics/i18n/
|
# -> https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "fr"
|
LANGUAGE_CODE = "fr-fr"
|
||||||
|
|
||||||
LANGUAGES = [
|
TIME_ZONE = "UTC"
|
||||||
("en", "English"),
|
|
||||||
("fr", "Français"),
|
|
||||||
]
|
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
|
||||||
(BASE_DIR / "shared" / "locale"),
|
|
||||||
]
|
|
||||||
|
|
||||||
TIME_ZONE = "Europe/Paris"
|
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
@ -306,7 +148,6 @@ USE_TZ = True
|
||||||
# -> https://docs.djangoproject.com/en/4.2/howto/static-files/
|
# -> https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
MEDIA_URL = "media/"
|
|
||||||
|
|
||||||
STATICFILES_DIRS = [BASE_DIR / "shared" / "static"]
|
STATICFILES_DIRS = [BASE_DIR / "shared" / "static"]
|
||||||
STATICFILES_FINDERS = [
|
STATICFILES_FINDERS = [
|
||||||
|
@ -315,13 +156,11 @@ STATICFILES_FINDERS = [
|
||||||
"sass_processor.finders.CssFinder",
|
"sass_processor.finders.CssFinder",
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_ROOT = credentials.get("STATIC_ROOT")
|
STATIC_ROOT = credentials["STATIC_ROOT"]
|
||||||
MEDIA_ROOT = credentials.get("MEDIA_ROOT")
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Storages configuration
|
# Storages configuration
|
||||||
|
|
||||||
ARCHIVES_INTERNAL = credentials.get("ARCHIVES_INTERNAL", "_archives")
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
@ -329,13 +168,6 @@ STORAGES = {
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||||
},
|
},
|
||||||
"archives": {
|
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
||||||
"OPTIONS": {
|
|
||||||
"location": credentials["ARCHIVES_ROOT"],
|
|
||||||
"base_url": f"/{ARCHIVES_INTERNAL}/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -354,41 +186,20 @@ SASS_PROCESSOR_ENABLED = True
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Messages configuration
|
|
||||||
|
|
||||||
MESSAGE_TAGS = {
|
|
||||||
messages.DEBUG: "is-dark",
|
|
||||||
messages.INFO: "is-primary",
|
|
||||||
messages.SUCCESS: "is-success",
|
|
||||||
messages.WARNING: "is-warning",
|
|
||||||
messages.ERROR: "is-danger",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# Unfold Interface configuration
|
|
||||||
|
|
||||||
UNFOLD = {
|
|
||||||
"SITE_HEADER": _("Administration de DGSI"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Extend settings when running in dev mode
|
# Extend settings when running in dev mode
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
"debug_toolbar",
|
"debug_toolbar",
|
||||||
"django_extensions",
|
"django_browser_reload",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE += [
|
MIDDLEWARE += [
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||||
|
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
|
||||||
|
|
||||||
INTERNAL_IPS = ["127.0.0.1"]
|
INTERNAL_IPS = ["127.0.0.1"]
|
||||||
|
|
||||||
DEBUG_TOOLBAR_CONFIG = {"INSERT_BEFORE": "</footer>"}
|
DEBUG_TOOLBAR_CONFIG = {"INSERT_BEFORE": "</footer>"}
|
||||||
|
|
|
@ -14,29 +14,31 @@ Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("", TemplateView.as_view(template_name="home.html"), name="index"),
|
||||||
|
path("login", TemplateView.as_view(template_name="login.html"), name="login"),
|
||||||
|
path(
|
||||||
|
"profiles.html",
|
||||||
|
login_required(TemplateView.as_view(template_name="profiles.html")),
|
||||||
|
name="profiles",
|
||||||
|
),
|
||||||
|
path("profiles/ios.xml", views.profile_ios, name="profiles-ios"),
|
||||||
path("", include("dgsi.urls")),
|
path("", include("dgsi.urls")),
|
||||||
path("api/", include("api.urls")),
|
|
||||||
path("accounts/login/link/", include("shared.authentication.urls")),
|
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
path("admin/", admin.site.urls),
|
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
|
||||||
path("__reload__/", include("django_browser_reload.urls")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("__reload__/", include("django_browser_reload.urls")),
|
||||||
path("__debug__/", include("debug_toolbar.urls")),
|
path("__debug__/", include("debug_toolbar.urls")),
|
||||||
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
|
||||||
*static(
|
|
||||||
settings.STORAGES["archives"]["OPTIONS"]["base_url"],
|
|
||||||
document_root=settings.STORAGES["archives"]["OPTIONS"]["location"],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
10
src/app/utils.py
Normal file
10
src/app/utils.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import kanidm
|
||||||
|
|
||||||
|
|
||||||
|
async def get_radius_credential(username):
|
||||||
|
config = kanidm.KanidmClientConfig(uri="https://sso.dgnum.eu")
|
||||||
|
client = kanidm.KanidmClient(config=config)
|
||||||
|
token = await client.get_radius_token(username)
|
||||||
|
return json.loads(token.content)["secret"]
|
18
src/app/views.py
Normal file
18
src/app/views.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from asgiref.sync import async_to_sync, sync_to_async
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
@sync_to_async
|
||||||
|
@login_required(login_url="/login/")
|
||||||
|
@async_to_sync
|
||||||
|
async def profile_ios(request):
|
||||||
|
radius_credential = await utils.get_radius_credential(request.user.username)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"iosprofile.xml",
|
||||||
|
{"radius_credential": radius_credential},
|
||||||
|
content_type="text/xml",
|
||||||
|
)
|
|
@ -7,6 +7,10 @@ For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
|
@ -1,71 +1 @@
|
||||||
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
|
# Register your models here.
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth.admin import GroupAdmin as DjangoGroupAdmin
|
|
||||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from dgsi.models import Archive, Bylaws, Service, Statutes, Translation, User
|
|
||||||
from shared.admin import BaseModelAdmin
|
|
||||||
|
|
||||||
assert DjangoUserAdmin.fieldsets is not None
|
|
||||||
|
|
||||||
|
|
||||||
def unregister(*models, site=None):
|
|
||||||
"""
|
|
||||||
Unregister the given model(s) classes.
|
|
||||||
|
|
||||||
unregister(Author)
|
|
||||||
|
|
||||||
The `site` kwarg is an admin site to use instead of the default admin site.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib.admin.sites import site as default_site
|
|
||||||
|
|
||||||
admin_site = site or default_site
|
|
||||||
|
|
||||||
if not isinstance(admin_site, admin.AdminSite):
|
|
||||||
raise ValueError("site must subclass AdminSite")
|
|
||||||
|
|
||||||
admin_site.unregister(models)
|
|
||||||
|
|
||||||
|
|
||||||
# Unregister allauth models
|
|
||||||
unregister(SocialAccount, SocialApp, SocialToken)
|
|
||||||
|
|
||||||
# Unregister django models
|
|
||||||
unregister(Group)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Group)
|
|
||||||
class GroupAdmin(DjangoGroupAdmin, BaseModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
|
||||||
class UserAdmin(DjangoUserAdmin, BaseModelAdmin):
|
|
||||||
readonly_fields = ("vlan_id",)
|
|
||||||
|
|
||||||
# Add the local fields
|
|
||||||
fieldsets = (
|
|
||||||
*DjangoUserAdmin.fieldsets,
|
|
||||||
(
|
|
||||||
_("Informations réseau"),
|
|
||||||
{"fields": ("vlan_id",)},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_("Documents DGNum"),
|
|
||||||
{"fields": ("accepted_statutes", "accepted_bylaws")},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for model in [
|
|
||||||
Archive,
|
|
||||||
Bylaws,
|
|
||||||
Service,
|
|
||||||
SocialAccount,
|
|
||||||
Statutes,
|
|
||||||
Translation,
|
|
||||||
]:
|
|
||||||
admin.site.register(model, BaseModelAdmin)
|
|
||||||
|
|
|
@ -4,7 +4,3 @@ from django.apps import AppConfig
|
||||||
class DgsiConfig(AppConfig):
|
class DgsiConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "dgsi"
|
name = "dgsi"
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
# Implicitely connect signal handlers
|
|
||||||
from shared.authentication import signals # noqa: F401
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.forms import BooleanField, CharField, EmailField, forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from shared.kanidm import klient
|
|
||||||
|
|
||||||
|
|
||||||
@async_to_sync
|
|
||||||
async def name_validator(value: str) -> None:
|
|
||||||
try:
|
|
||||||
await klient.person_account_get(value)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValidationError(_("Identifiant déjà présent dans la base de données."))
|
|
||||||
|
|
||||||
|
|
||||||
class CreateKanidmAccountForm(forms.Form):
|
|
||||||
# TODO: Add a field for the clipper login information for the local mapping
|
|
||||||
name = CharField(
|
|
||||||
label=_("Identifiant"),
|
|
||||||
help_text=_("De préférence identique au login ENS de la personne concernée."),
|
|
||||||
validators=[name_validator],
|
|
||||||
)
|
|
||||||
displayname = CharField(label=_("Nom d'usage"))
|
|
||||||
mail = EmailField(
|
|
||||||
label=_("Adresse e-mail"),
|
|
||||||
help_text=_(
|
|
||||||
"De préférence :<br>"
|
|
||||||
"- l'adresse <code>@ens.psl.eu</code> pour les personnes en scolarité ;<br>"
|
|
||||||
"- l'adresse <code>@normalesup.org</code> pour les personnes ayant fini leur scolarité ;<br>"
|
|
||||||
"<b>Pour les personnes extérieures, le bureau doit donner son accord.</b>"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
active = BooleanField(
|
|
||||||
label=_("Membre actif"),
|
|
||||||
help_text=_(
|
|
||||||
"Si selectionné, la personne sera ajoutée au groupe <code>dgnum_members</code>.<br>"
|
|
||||||
"<b>L'accord préalable du bureau est nécessaire !</b>"
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateSelfAccountForm(forms.Form):
|
|
||||||
displayname = CharField(label=_("Nom d'usage"))
|
|
||||||
mail = EmailField(
|
|
||||||
label=_("Adresse e-mail"),
|
|
||||||
help_text=_("De préférence l'adresse '@ens.psl.eu'"),
|
|
||||||
)
|
|
|
@ -1,131 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-14 13:35
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("auth", "0012_alter_user_first_name_max_length"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="User",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
|
||||||
(
|
|
||||||
"last_login",
|
|
||||||
models.DateTimeField(
|
|
||||||
blank=True, null=True, verbose_name="last login"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_superuser",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
|
||||||
verbose_name="superuser status",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"username",
|
|
||||||
models.CharField(
|
|
||||||
error_messages={
|
|
||||||
"unique": "A user with that username already exists."
|
|
||||||
},
|
|
||||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
|
||||||
max_length=150,
|
|
||||||
unique=True,
|
|
||||||
validators=[
|
|
||||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
|
||||||
],
|
|
||||||
verbose_name="username",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"first_name",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, max_length=150, verbose_name="first name"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"last_name",
|
|
||||||
models.CharField(
|
|
||||||
blank=True, max_length=150, verbose_name="last name"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"email",
|
|
||||||
models.EmailField(
|
|
||||||
blank=True, max_length=254, verbose_name="email address"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_staff",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Designates whether the user can log into this admin site.",
|
|
||||||
verbose_name="staff status",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_active",
|
|
||||||
models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
|
||||||
verbose_name="active",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"date_joined",
|
|
||||||
models.DateTimeField(
|
|
||||||
default=django.utils.timezone.now, verbose_name="date joined"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"groups",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
|
||||||
related_name="user_set",
|
|
||||||
related_query_name="user",
|
|
||||||
to="auth.group",
|
|
||||||
verbose_name="groups",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user_permissions",
|
|
||||||
models.ManyToManyField(
|
|
||||||
blank=True,
|
|
||||||
help_text="Specific permissions for this user.",
|
|
||||||
related_name="user_set",
|
|
||||||
related_query_name="user",
|
|
||||||
to="auth.permission",
|
|
||||||
verbose_name="user permissions",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "user",
|
|
||||||
"verbose_name_plural": "users",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
("objects", django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-23 15:03
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Service",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(
|
|
||||||
max_length=255, verbose_name="Nom du service proposé"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("url", models.URLField(verbose_name="Adresse du service")),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-23 21:29
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0002_service"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="service",
|
|
||||||
name="icon",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True, max_length=255, verbose_name="Icône du service"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,81 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-24 08:07
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0003_service_icon"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Bylaws",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("date", models.DateField(verbose_name="Date du document")),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(max_length=255, verbose_name="Nom du document"),
|
|
||||||
),
|
|
||||||
("file", models.FileField(upload_to="", verbose_name="Fichier PDF")),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"get_latest_by": "date",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Statutes",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("date", models.DateField(verbose_name="Date du document")),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(max_length=255, verbose_name="Nom du document"),
|
|
||||||
),
|
|
||||||
("file", models.FileField(upload_to="", verbose_name="Fichier PDF")),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"get_latest_by": "date",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="accepted_bylaws",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="dgsi.bylaws",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="accepted_statutes",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="dgsi.statutes",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,28 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-24 20:22
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0004_bylaws_statutes_user_accepted_bylaws_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="bylaws",
|
|
||||||
options={
|
|
||||||
"get_latest_by": "date",
|
|
||||||
"verbose_name": "Règlement Intérieur",
|
|
||||||
"verbose_name_plural": "Règlements Intérieurs",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="statutes",
|
|
||||||
options={
|
|
||||||
"get_latest_by": "date",
|
|
||||||
"verbose_name": "Statuts",
|
|
||||||
"verbose_name_plural": "Statuts",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Generated by Django 4.2.12 on 2024-09-26 09:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0005_alter_bylaws_options_alter_statutes_options"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Translation",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("cas_login", models.CharField(max_length=255, unique=True)),
|
|
||||||
("username", models.CharField(max_length=255, unique=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Correspondance de login",
|
|
||||||
"verbose_name_plural": "Correspondances de login",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Generated by Django 4.2.16 on 2024-09-27 10:40
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0006_translation"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="accepted_bylaws",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="dgsi.bylaws",
|
|
||||||
verbose_name="Dernier Règlement Intérieur accepté",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="accepted_statutes",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="dgsi.statutes",
|
|
||||||
verbose_name="Derniers statuts acceptés",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,25 +0,0 @@
|
||||||
# Generated by Django 4.2.16 on 2024-10-12 20:04
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0007_alter_user_accepted_bylaws_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="accepted_statutes",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="dgsi.statutes",
|
|
||||||
verbose_name="Derniers Statuts acceptés",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,46 +0,0 @@
|
||||||
# Generated by Django 4.2.16 on 2025-01-25 15:27
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import dgsi.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0008_alter_user_accepted_statutes"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Archive",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("date", models.DateField(verbose_name="Date du document")),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(max_length=255, verbose_name="Nom du document"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"file",
|
|
||||||
models.FileField(
|
|
||||||
storage=dgsi.models.get_storage,
|
|
||||||
upload_to="",
|
|
||||||
verbose_name="Fichier PDF",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Document d'archives",
|
|
||||||
"verbose_name_plural": "Documents d'archives",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Generated by Django 4.2.16 on 2025-01-30 08:20
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("dgsi", "0009_archive"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="user",
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="vlan_id",
|
|
||||||
field=models.PositiveSmallIntegerField(
|
|
||||||
null=True, verbose_name="VLAN associé au compte"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="user",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("vlan_id",), name="unique_vlan_attribution"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,82 +0,0 @@
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import AccessMixin, UserPassesTestMixin
|
|
||||||
from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
|
||||||
|
|
||||||
from dgsi.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class KanidmAccountRequiredMixin(AccessMixin):
|
|
||||||
"""
|
|
||||||
Mixin used to require the existence of a kanidm account.
|
|
||||||
"""
|
|
||||||
|
|
||||||
require_radius_secret: bool = False
|
|
||||||
|
|
||||||
def dispatch(
|
|
||||||
self, request: HttpRequest, *args: Any, **kwargs: Any
|
|
||||||
) -> HttpResponseBase:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
|
|
||||||
self._user = User.from_request(request)
|
|
||||||
|
|
||||||
if self._user.kanidm is None:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_("<b>Veuillez créer un compte DGNum.</b>"),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy("dgsi:create_self_account"))
|
|
||||||
|
|
||||||
if self.require_radius_secret and self._user.kanidm.radius_secret is None:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_("<b>Veuillez générer un mot de passe Wi-Fi.</b>"),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy("dgsi:profile"))
|
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class StaffRequiredMixin(UserPassesTestMixin):
|
|
||||||
request: HttpRequest
|
|
||||||
|
|
||||||
def test_func(self) -> bool:
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
assert isinstance(self.request.user, User)
|
|
||||||
|
|
||||||
return self.request.user.is_staff
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
# NOTE: We are only allowed to do this if a class is supplied to the right when constructing the view
|
|
||||||
return super().get_context_data(admin_view=True, **kwargs) # pyright: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class HtmxPostMixin(TemplateResponseMixin, ContextMixin):
|
|
||||||
http_method_names = ["post"]
|
|
||||||
|
|
||||||
def execute_action(self, *args, **kwargs) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
# Execute action
|
|
||||||
self.execute_action()
|
|
||||||
|
|
||||||
context = self.get_context_data(**kwargs)
|
|
||||||
return self.render_to_response(context)
|
|
||||||
|
|
||||||
|
|
||||||
class HtmxPostObjectMixin(SingleObjectMixin, HtmxPostMixin):
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.object = self.get_object()
|
|
||||||
return super().post(request, *args, **kwargs)
|
|
|
@ -1,255 +1 @@
|
||||||
import logging
|
# Create your models here.
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import cached_property
|
|
||||||
from typing import Optional, Self
|
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
|
||||||
from allauth.socialaccount.models import SocialAccount
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.core.files import storage
|
|
||||||
from django.db import models, transaction
|
|
||||||
from django.db.models.signals import pre_delete
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from kanidm.exceptions import NoMatchingEntries
|
|
||||||
from kanidm.models.person import Person
|
|
||||||
|
|
||||||
from shared.kanidm import klient
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Service(models.Model):
|
|
||||||
name = models.CharField(_("Nom du service proposé"), max_length=255)
|
|
||||||
url = models.URLField(_("Adresse du service"))
|
|
||||||
icon = models.CharField(_("Icône du service"), max_length=255, blank=True)
|
|
||||||
|
|
||||||
# TODO: Add a group field, to show only if the user really has access,
|
|
||||||
# when this field is null, then it is open bar
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.name} [{self.url}]"
|
|
||||||
|
|
||||||
|
|
||||||
class LegalDocument(models.Model):
|
|
||||||
date = models.DateField(_("Date du document"))
|
|
||||||
name = models.CharField(_("Nom du document"), max_length=255)
|
|
||||||
file = models.FileField(_("Fichier PDF"))
|
|
||||||
|
|
||||||
icon = "script"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def latest(cls, **kwargs) -> Self | None:
|
|
||||||
return cls.objects.filter(**kwargs).latest()
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta: # pyright: ignore
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Statutes(LegalDocument):
|
|
||||||
"""
|
|
||||||
Statutes of the association.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kind = "statutes"
|
|
||||||
color = "primary"
|
|
||||||
|
|
||||||
class Meta: # pyright: ignore
|
|
||||||
get_latest_by = "date"
|
|
||||||
verbose_name = _("Statuts")
|
|
||||||
verbose_name_plural = _("Statuts")
|
|
||||||
|
|
||||||
|
|
||||||
class Bylaws(LegalDocument):
|
|
||||||
"""
|
|
||||||
Bylaws of the association.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kind = "bylaws"
|
|
||||||
color = "success"
|
|
||||||
|
|
||||||
class Meta: # pyright: ignore
|
|
||||||
get_latest_by = "date"
|
|
||||||
verbose_name = _("Règlement Intérieur")
|
|
||||||
verbose_name_plural = _("Règlements Intérieurs")
|
|
||||||
|
|
||||||
|
|
||||||
def get_storage(*args, **kwargs):
|
|
||||||
return storage.storages["archives"]
|
|
||||||
|
|
||||||
|
|
||||||
class Archive(models.Model):
|
|
||||||
"""
|
|
||||||
Archived documents for the association.
|
|
||||||
"""
|
|
||||||
|
|
||||||
date = models.DateField(_("Date du document"))
|
|
||||||
name = models.CharField(_("Nom du document"), max_length=255)
|
|
||||||
file = models.FileField(_("Fichier PDF"), storage=get_storage)
|
|
||||||
|
|
||||||
icon = "archive"
|
|
||||||
color = "warning"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta: # pyright: ignore
|
|
||||||
verbose_name = _("Document d'archives")
|
|
||||||
verbose_name_plural = _("Documents d'archives")
|
|
||||||
|
|
||||||
|
|
||||||
# class TermsAndConditions(LegalDocument):
|
|
||||||
# """
|
|
||||||
# Terms and Conditions of use regarding a service offered by the association.
|
|
||||||
# """
|
|
||||||
#
|
|
||||||
# service = models.ForeignKey(Service, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
class Translation(models.Model):
|
|
||||||
cas_login = models.CharField(max_length=255, unique=True)
|
|
||||||
username = models.CharField(max_length=255, unique=True)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.cas_login} → {self.username}"
|
|
||||||
|
|
||||||
def update_user(self, username: str):
|
|
||||||
# Update the username of the person with the required cas_login
|
|
||||||
try:
|
|
||||||
# Find out if a user exists with the cas_login to update
|
|
||||||
account = SocialAccount.objects.get(provider="ens_cas", uid=self.cas_login)
|
|
||||||
|
|
||||||
# WARNING: This updates the remote data, we need to be careful with what we do
|
|
||||||
async_to_sync(klient.person_account_update)(account.user.username, username)
|
|
||||||
|
|
||||||
account.user.username = username
|
|
||||||
account.user.save()
|
|
||||||
except SocialAccount.DoesNotExist:
|
|
||||||
# No user has registered with this cas_login yet
|
|
||||||
pass
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs) -> None:
|
|
||||||
# INFO: Only update the model if it does not already exist
|
|
||||||
# This will prevent a lot of pain
|
|
||||||
if self.pk is None:
|
|
||||||
self.update_user(self.username)
|
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta: # pyright: ignore
|
|
||||||
verbose_name = _("Correspondance de login")
|
|
||||||
verbose_name_plural = _("Correspondances de login")
|
|
||||||
|
|
||||||
|
|
||||||
# INFO: We need to use a signal receiver here, as the delete method is not called
|
|
||||||
# when deleting objects from the admin interface
|
|
||||||
@receiver(pre_delete, sender=Translation)
|
|
||||||
def restore_username(**kwargs):
|
|
||||||
"""
|
|
||||||
Restore the username to the cas_login
|
|
||||||
"""
|
|
||||||
|
|
||||||
self = kwargs["instance"]
|
|
||||||
self.update_user(self.cas_login)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class KanidmProfile:
|
|
||||||
person: Person
|
|
||||||
radius_secret: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
|
||||||
"""
|
|
||||||
Custom User class, to have a direct link to the Kanidm data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
accepted_statutes = models.ForeignKey(
|
|
||||||
Statutes,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
verbose_name=_("Derniers Statuts acceptés"),
|
|
||||||
)
|
|
||||||
accepted_bylaws = models.ForeignKey(
|
|
||||||
Bylaws,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
verbose_name=_("Dernier Règlement Intérieur accepté"),
|
|
||||||
)
|
|
||||||
# accepted_terms = models.ManyToManyField(TermsAndConditions)
|
|
||||||
|
|
||||||
vlan_id = models.PositiveSmallIntegerField(_("VLAN associé au compte"), null=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_request(cls, request: HttpRequest) -> Self:
|
|
||||||
u = request.user
|
|
||||||
|
|
||||||
assert isinstance(u, cls)
|
|
||||||
|
|
||||||
return u
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
@async_to_sync
|
|
||||||
async def kanidm(self) -> Optional[KanidmProfile]:
|
|
||||||
try:
|
|
||||||
radius_data = (await klient.get_radius_token(self.username)).data
|
|
||||||
|
|
||||||
return KanidmProfile(
|
|
||||||
person=(await klient.person_account_get(self.username)),
|
|
||||||
radius_secret=radius_data and radius_data.get("secret"),
|
|
||||||
)
|
|
||||||
except NoMatchingEntries:
|
|
||||||
return None
|
|
||||||
except (TimeoutError, ClientConnectorError) as e:
|
|
||||||
logging.error(f"Erreur lors de la requête à Kanidm: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def part_of(self, group: str) -> bool:
|
|
||||||
return (self.kanidm is not None) and (group in self.kanidm.person.memberof)
|
|
||||||
|
|
||||||
def can_access_archive(self, archive: Archive) -> bool:
|
|
||||||
# Prepare a more complex workflow
|
|
||||||
return True
|
|
||||||
|
|
||||||
###
|
|
||||||
# VLAN attribution machinery
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def register_unique_vlan(self):
|
|
||||||
if self.vlan_id is not None:
|
|
||||||
raise ValueError(_("Ce compte a déjà un VLAN associé"))
|
|
||||||
|
|
||||||
self.vlan_id = min(
|
|
||||||
set(range(settings.VLAN_ID_MIN, settings.VLAN_ID_MAX))
|
|
||||||
- set(
|
|
||||||
User.objects.exclude(vlan_id__isnull=True).values_list(
|
|
||||||
"vlan_id", flat=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.save(update_fields=["vlan_id"])
|
|
||||||
|
|
||||||
def reclaim_vlan(self):
|
|
||||||
if self.vlan_id is None:
|
|
||||||
# Nothing to do, just return
|
|
||||||
logger.warning(
|
|
||||||
f"Reclaiming VLAN for {self.username} who does not have one."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.vlan_id = None
|
|
||||||
self.save(update_fields=["vlan_id"])
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(fields=["vlan_id"], name="unique_vlan_attribution")
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<a class="button bt-link is-light {{ link.color }}"
|
|
||||||
href="{% if link.absolute %}{{ link.reverse }}{% else %}{% url link.reverse %}{% endif %}">
|
|
||||||
{% if link.icon %}<span class="icon"><i class="ti ti-{{ link.icon }}"></i></span>{% endif %}
|
|
||||||
<span>{{ link.text }}</span>
|
|
||||||
</a>
|
|
|
@ -1,26 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<h2 class="subtitle">
|
|
||||||
{{ title }}
|
|
||||||
<span class="tags is-pulled-right">
|
|
||||||
{% if user_document != document %}
|
|
||||||
<a class="tag is-warning"
|
|
||||||
href="{% url "dgsi:accept_legal_document" document.kind %}"
|
|
||||||
onclick="return confirm(('{% trans " En acceptant, vous assurez avoir lu ce document et en approuver le contenu." %}'))">
|
|
||||||
<span>{{ accept_question }}</span>
|
|
||||||
<span class="icon is-size-6"><i class="ti ti-alert-circle"></i></span>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="tag is-success">
|
|
||||||
<span>{% trans "Accepté" %}</span>
|
|
||||||
<span class="icon is-size-6"><i class="ti ti-checkbox"></i></span>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="tag is-dark">{{ document.date }}</span>
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<a class="button bt-link" href="{{ document.file.url }}">
|
|
||||||
<span class="ellipsis">{{ document }}</span>
|
|
||||||
<span class="icon"><i class="ti ti-file-download"></i></span>
|
|
||||||
</a>
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<h2 class="subtitle">
|
|
||||||
{% trans subtitle %}
|
|
||||||
<a class="button is-small is-pulled-right is-primary" href="{% url backlink|default:'dgsi:index' %}">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="ti ti-arrow-big-left-filled"></i>
|
|
||||||
</span>
|
|
||||||
<span>{% trans "Retour" %}</span>
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<hr>
|
|
|
@ -1,83 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>ConsentText</key>
|
|
||||||
<dict>
|
|
||||||
<key>default</key>
|
|
||||||
<string>Souhaitez-vous configurer votre appareil pour utiliser le Wi-Fi DGNum ?</string>
|
|
||||||
<key>en</key>
|
|
||||||
<string>Dou you want to configure your device to use the DGNum Wi-Fi ?</string>
|
|
||||||
</dict>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>304283f2-b4df-4f54-9fd9-9c8e1fdc778f</string>
|
|
||||||
<key>PayloadContent</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>AutoJoin</key>
|
|
||||||
<true/>
|
|
||||||
<key>CaptiveBypass</key>
|
|
||||||
<true/>
|
|
||||||
<key>EAPClientConfiguration</key>
|
|
||||||
<dict>
|
|
||||||
<key>AcceptEAPTypes</key>
|
|
||||||
<array>
|
|
||||||
<integer>25</integer>
|
|
||||||
</array>
|
|
||||||
<key>OuterIdentity</key>
|
|
||||||
<string>anonymous</string>
|
|
||||||
<key>TLSMaximumVersion</key>
|
|
||||||
<string>1.3</string>
|
|
||||||
<key>TLSMinimumVersion</key>
|
|
||||||
<string>1.2</string>
|
|
||||||
<key>TLSTrustedServerNames</key>
|
|
||||||
<array>
|
|
||||||
<string>radius.dgnum.eu</string>
|
|
||||||
</array>
|
|
||||||
<key>UserName</key>
|
|
||||||
<string>{{ user.username }}</string>
|
|
||||||
<key>UserPassword</key>
|
|
||||||
<string>{{ user.kanidm.radius_secret }}</string>
|
|
||||||
<key>TTLSInnerAuthentication</key>
|
|
||||||
<string>MSCHAPv2</string>
|
|
||||||
</dict>
|
|
||||||
<key>PayloadUUID</key>
|
|
||||||
<string>a9a6e20c-1d9e-497a-b10c-f93a62e3e7df</string>
|
|
||||||
<key>EncryptionType</key>
|
|
||||||
<string>WPA2</string>
|
|
||||||
<key>HIDDEN_NETWORK</key>
|
|
||||||
<false/>
|
|
||||||
<key>IsHotspot</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>DGNum Wi-Fi configuration</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Wi-Fi</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>com.apple.wifi.managed.a9a6e20c-1d9e-497a-b10c-f93a62e3e7df</string>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>com.apple.wifi.managed</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
<key>ProxyType</key>
|
|
||||||
<string>None</string>
|
|
||||||
<key>SSID_STR</key>
|
|
||||||
<string>{{ dgnum_ssid }}</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>PayloadDescription</key>
|
|
||||||
<string>DGNum Wi-Fi configuration</string>
|
|
||||||
<key>PayloadDisplayName</key>
|
|
||||||
<string>Wi-Fi DGNum</string>
|
|
||||||
<key>PayloadIdentifier</key>
|
|
||||||
<string>dgnum-radius.304283f2-b4df-4f54-9fd9-9c8e1fdc778f</string>
|
|
||||||
<key>PayloadOrganization</key>
|
|
||||||
<string>Délégation Générale Numérique</string>
|
|
||||||
<key>PayloadRemovalDisallowed</key>
|
|
||||||
<false/>
|
|
||||||
<key>PayloadType</key>
|
|
||||||
<string>Configuration</string>
|
|
||||||
<key>PayloadVersion</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Archives de la DGNum" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
{% for file in document_list %}
|
|
||||||
<a class="button bt-archive"
|
|
||||||
{% if file.kind == "statutes" or file.kind == "bylaws" %} href="{{ file.file.url }}" {% else %} href="{% url "dgsi:protected_archive" file.pk %}" {% endif %}>
|
|
||||||
<span class="tag is-{{ file.color }} is-pulled-left">
|
|
||||||
<span class="icon"><i class="ti ti-{{ file.icon }}"></i></span>
|
|
||||||
</span>
|
|
||||||
<span class="ellipsis mx-2">{{ file }}</span>
|
|
||||||
<span class="tag is-pulled-right">{{ file.date }}</span>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Création de compte Kanidm" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% include "bulma/form.html" with form=form %}
|
|
||||||
|
|
||||||
<button class="button is-fullwidth mt-6">
|
|
||||||
<span class="icon"><i class="ti ti-check"></i></span>
|
|
||||||
<span>{% trans "Enregistrer" %}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Création d'un compte DGNum" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% include "bulma/form.html" with form=form %}
|
|
||||||
|
|
||||||
<button class="button is-fullwidth mt-6">
|
|
||||||
<span class="icon"><i class="ti ti-check"></i></span>
|
|
||||||
<span>{% trans "Enregistrer" %}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
{% for link in links.authenticated %}
|
|
||||||
{% include "_index_link.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user.is_staff %}
|
|
||||||
<hr>
|
|
||||||
{% for link in links.admin %}
|
|
||||||
{% include "_index_link.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,32 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Documents légaux" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
{% if user.kanidm is None %}
|
|
||||||
{% if show_message %}
|
|
||||||
<div class="notification is-warning is-light has-text-centered">
|
|
||||||
<b>{% trans "Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant de pouvoir créer un compte." %}</b>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="notification is-primary is-light has-text-centered">
|
|
||||||
<b>{% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %}</b>
|
|
||||||
<br>
|
|
||||||
<a class="button mt-5 is-light"
|
|
||||||
href="{% url "dgsi:create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<br class="my-5">
|
|
||||||
|
|
||||||
{% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %}
|
|
||||||
|
|
||||||
<br class="my-4">
|
|
||||||
|
|
||||||
{% include "_legal_document.html" with document=bylaws user_document=user.accepted_bylaws title=_("Règlement Intérieur") accept_question=_("Accepter le règlement intérieur") %}
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
|
@ -1,42 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "_subtitle.html" with subtitle="Mentions Légales" %}
|
|
||||||
|
|
||||||
<section class="section content">
|
|
||||||
<p class="is-size-4">Éditeur</p>
|
|
||||||
<p>Ce site web est édité par la Délégation Générale Numérique.</p>
|
|
||||||
<p>
|
|
||||||
<b>Délégation Générale Numérique (DGNum)</b>
|
|
||||||
<br>
|
|
||||||
Association de loi 1901
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Siège social :
|
|
||||||
<br>
|
|
||||||
<i>45 rue d'Ulm, 75005 Paris - France</i>
|
|
||||||
</p>
|
|
||||||
<p>Directeur de publication : Jean-Marc Gailis</p>
|
|
||||||
<p>Contact : contact[at]dgnum.eu</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<p class="is-size-4">Hébergeur</p>
|
|
||||||
<p>Ce site web est hébergé par la Délégation Générale Numérique.</p>
|
|
||||||
<p>
|
|
||||||
<b>Délégation Générale Numérique (DGNum)</b>
|
|
||||||
<br>
|
|
||||||
Association de loi 1901
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Siège social :
|
|
||||||
<br>
|
|
||||||
<i>45 rue d'Ulm, 75005 Paris - France</i>
|
|
||||||
</p>
|
|
||||||
<p>Directeur de publication : Jean-Marc Gailis</p>
|
|
||||||
<p>Contact : contact[at]dgnum.eu</p>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
|
@ -1,30 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% if user.kanidm %}
|
|
||||||
<h3 class="has-text-weight-bold mb-3">
|
|
||||||
<span>{% trans "Mot de passe WiFi :" %}</span>
|
|
||||||
{% if user.kanidm.radius_secret %}
|
|
||||||
<a class="button is-small is-danger is-pulled-right"
|
|
||||||
hx-post="{% url "dgsi:generate_wifi_password" %}"
|
|
||||||
hx-confirm="{% trans "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?" %}">
|
|
||||||
<span class="icon"><i class="ti ti-refresh"></i></span>
|
|
||||||
<span class="has-text-weight-normal">{% trans "Réinitialiser le mot de passe WiFi" %}</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{% if user.kanidm.radius_secret %}
|
|
||||||
<div class="buttons has-addons is-flex">
|
|
||||||
<input id="radius-secret"
|
|
||||||
data-select
|
|
||||||
class="button is-primary is-size-4 is-flex-grow-2"
|
|
||||||
value="{{ user.kanidm.radius_secret }}"
|
|
||||||
type="password"
|
|
||||||
readonly />
|
|
||||||
<a id="secret-toggle" class="button is-size-4 is-warning is-light"><span class="icon"><i class="ti ti-eye"></i></span></a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<a hx-post="{% url "dgsi:generate_wifi_password" %}"
|
|
||||||
class="button is-fullwidth is-primary is-light is-size-4 block">{% trans "Générer un mot de passe WiFi" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<tr id="user-{{ person.pk }}">
|
|
||||||
<th>{{ person.username }}</th>
|
|
||||||
<td>{{ person.first_name }} {{ person.last_name }}</td>
|
|
||||||
<td>{{ person.email }}</td>
|
|
||||||
<td>{{ person.vlan_id|default:"" }}</td>
|
|
||||||
<td>
|
|
||||||
{% if person.vlan_id %}
|
|
||||||
<a hx-post="{% url "dgsi:user_deassign_vlan" person.pk %}"
|
|
||||||
hx-target="#user-{{ person.pk }}"
|
|
||||||
class="button is-fullwidth is-light is-warning">{% trans "Désallouer" %}</a>
|
|
||||||
{% elif person.kanidm %}
|
|
||||||
<a hx-post="{% url "dgsi:user_assign_vlan" person.pk %}"
|
|
||||||
hx-target="#user-{{ person.pk }}"
|
|
||||||
class="button is-fullwidth is-light is-primary">{% trans "Allouer" %}</a>
|
|
||||||
{% else %}
|
|
||||||
<button class="button is-fullwidth is-static">{% trans "Pas de compte" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
|
@ -1,112 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const secret = document.getElementById("radius-secret");
|
|
||||||
const toggle = document.getElementById("secret-toggle");
|
|
||||||
|
|
||||||
toggle.addEventListener("click", () => {
|
|
||||||
if (secret.type === "password") {
|
|
||||||
secret.type = "text";
|
|
||||||
toggle.innerHTML = `<span class="icon"><i class="ti ti-eye-off"></i></span>`;
|
|
||||||
} else {
|
|
||||||
secret.type = "password";
|
|
||||||
toggle.innerHTML = `<span class="icon"><i class="ti ti-eye"></i></span>`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock extra_head %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Profil personnel" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "Nom d'utilisateur :" %}</h3>
|
|
||||||
<input data-select
|
|
||||||
class="button is-fullwidth"
|
|
||||||
value="{{ user.username }}"
|
|
||||||
readonly />
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{% include "dgsi/partials/profile-radius_secret.html" %}
|
|
||||||
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "Nom d'usage :" %}</h3>
|
|
||||||
<input data-select
|
|
||||||
class="button is-fullwidth"
|
|
||||||
value="{{ user.first_name }} {{ user.last_name|upper }}"
|
|
||||||
readonly />
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "Adresse e-mail :" %}</h3>
|
|
||||||
<input data-select
|
|
||||||
class="button is-fullwidth"
|
|
||||||
value="{{ user.email }}"
|
|
||||||
readonly />
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{% if user.kanidm and user.kanidm.radius_secret %}
|
|
||||||
<div class="buttons">
|
|
||||||
<a href="{% url "dgsi:apple_profile" %}" class="button is-light">
|
|
||||||
<span class="icon"><i class="ti ti-brand-apple-filled"></i></span>
|
|
||||||
<span>{% trans "Télécharger le profil Wi-Fi DGNum pour iOS, iPadOS et macOS" %}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user.kanidm %}
|
|
||||||
<h2 class="subtitle mt-4">
|
|
||||||
{% trans "Informations techniques" %}
|
|
||||||
<a class="button is-small is-primary is-light is-pulled-right"
|
|
||||||
data-toggle="on"
|
|
||||||
data-target="#technical-info"
|
|
||||||
data-class="is-hidden"
|
|
||||||
data-on-html="<span>{% trans "Afficher" %}</span>"
|
|
||||||
data-off-html="<span>{% trans "Cacher" %}</span>">{% trans "Afficher" %}</a>
|
|
||||||
</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div id="technical-info" class="is-hidden">
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "Identifiant interne :" %}</h3>
|
|
||||||
|
|
||||||
<input data-select
|
|
||||||
class="button is-fullwidth is-light"
|
|
||||||
value="{{ user.kanidm.person.uuid }}"
|
|
||||||
readonly />
|
|
||||||
<br>
|
|
||||||
|
|
||||||
{% if user.vlan_id %}
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "VLAN attribué :" %}</h3>
|
|
||||||
<input data-select
|
|
||||||
class="button is-fullwidth"
|
|
||||||
value="{{ user.vlan_id }}"
|
|
||||||
readonly />
|
|
||||||
<br>
|
|
||||||
{% else %}
|
|
||||||
<div class="notification is-warning is-light has-text-centered">
|
|
||||||
<b>{% trans "Pas de VLAN attribué." %}</b>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3 class="has-text-weight-bold mb-3">{% trans "Membre des groupes suivants :" %}</h3>
|
|
||||||
|
|
||||||
<div class="grid groups">
|
|
||||||
{% for group in user.kanidm.person.memberof %}
|
|
||||||
<div class="cell button is-static">
|
|
||||||
<span>{{ group }}</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="notification is-primary is-light has-text-centered mt-6">
|
|
||||||
<b>{% trans "Pas de compte DGNum répertorié." %}</b>
|
|
||||||
<br>
|
|
||||||
<a class="button mt-5 is-light"
|
|
||||||
href="{% url "dgsi:create_self_account" %}">{% trans "Créer un compte DGNum" %}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock content %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Services accessibles via la DGNum" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
<div class="buttons bt-links">
|
|
||||||
{% for service in service_list %}
|
|
||||||
<a class="button is-medium"
|
|
||||||
href="{% url "dgsi:service_redirect" service.pk %}">
|
|
||||||
<span class="icon"><i class="ti ti-{{ service.icon }}"></i></span>
|
|
||||||
<span>{{ service.name }}</span>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "_subtitle.html" with subtitle="Comptes DG·SI" %}
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table class="table is-fullwidth is-striped is-narrow">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Nom d'utilisateur" %}</th>
|
|
||||||
<th>{% trans "Nom d'usage" %}</th>
|
|
||||||
<th>{% trans "Adresse e-mail" %}</th>
|
|
||||||
<th>{% trans "VLAN attribué" %}</th>
|
|
||||||
<th>{% trans "Gestion du VLAN" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="is-centered">
|
|
||||||
{% for person in user_list %}
|
|
||||||
{% include "dgsi/partials/user_list-user.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
107
src/dgsi/urls.py
107
src/dgsi/urls.py
|
@ -1,106 +1 @@
|
||||||
from django.urls import path
|
urlpatterns = []
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = "dgsi"
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
###
|
|
||||||
# Miscelleanous views
|
|
||||||
path(
|
|
||||||
"",
|
|
||||||
views.IndexView.as_view(),
|
|
||||||
name="index",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"mentions-legales/",
|
|
||||||
TemplateView.as_view(template_name="dgsi/mentions_legales.html"),
|
|
||||||
name="mentions_legales",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/forbidden/",
|
|
||||||
views.TemplateView.as_view(template_name="accounts/forbidden_category.html"),
|
|
||||||
name="forbidden_account",
|
|
||||||
),
|
|
||||||
###
|
|
||||||
# Archives views
|
|
||||||
path(
|
|
||||||
"archives/",
|
|
||||||
views.ArchiveListView.as_view(),
|
|
||||||
name="archive_list",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"archives/<int:pk>/",
|
|
||||||
views.ProtectedArchiveView.as_view(),
|
|
||||||
name="protected_archive",
|
|
||||||
),
|
|
||||||
###
|
|
||||||
# Legal documents
|
|
||||||
path(
|
|
||||||
"legal-documents/",
|
|
||||||
views.LegalDocumentsView.as_view(),
|
|
||||||
name="legal_documents",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"legal-documents/accept/<slug:kind>/",
|
|
||||||
views.AcceptLegalDocumentView.as_view(),
|
|
||||||
name="accept_legal_document",
|
|
||||||
),
|
|
||||||
###
|
|
||||||
# Services views
|
|
||||||
path(
|
|
||||||
"services/",
|
|
||||||
views.ServiceListView.as_view(),
|
|
||||||
name="service_list",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"services/redirect/<int:pk>/",
|
|
||||||
views.ServiceRedirectView.as_view(),
|
|
||||||
name="service_redirect",
|
|
||||||
),
|
|
||||||
###
|
|
||||||
# Profile views
|
|
||||||
path(
|
|
||||||
"accounts/profile/",
|
|
||||||
views.ProfileView.as_view(),
|
|
||||||
name="profile",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/profile/apple/",
|
|
||||||
views.AppleProfileView.as_view(),
|
|
||||||
name="apple_profile",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/generate-wifi-password/",
|
|
||||||
views.GenerateWiFiPasswordView.as_view(),
|
|
||||||
name="generate_wifi_password",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/create/",
|
|
||||||
views.CreateSelfAccountView.as_view(),
|
|
||||||
name="create_self_account",
|
|
||||||
),
|
|
||||||
###
|
|
||||||
# Accounts admin views
|
|
||||||
path(
|
|
||||||
"accounts/create-kanidm/",
|
|
||||||
views.CreateKanidmAccountView.as_view(),
|
|
||||||
name="create_kanidm_account",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/list/",
|
|
||||||
views.UserListView.as_view(),
|
|
||||||
name="user_list",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/assign-vlan/<int:pk>",
|
|
||||||
views.UserAssignVlanView.as_view(),
|
|
||||||
name="user_assign_vlan",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"accounts/deassign-vlan/<int:pk>",
|
|
||||||
views.UserDeassignVlanView.as_view(),
|
|
||||||
name="user_deassign_vlan",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,374 +1 @@
|
||||||
from mimetypes import guess_type
|
# Create your views here.
|
||||||
from typing import Any, NamedTuple
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.core.mail import EmailMessage
|
|
||||||
from django.http import Http404, HttpRequest, HttpResponseBase, HttpResponseRedirect
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.functional import Promise
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.generic import FormView, ListView, RedirectView, TemplateView, View
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
|
||||||
|
|
||||||
from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm
|
|
||||||
from dgsi.mixins import (
|
|
||||||
HtmxPostObjectMixin,
|
|
||||||
KanidmAccountRequiredMixin,
|
|
||||||
StaffRequiredMixin,
|
|
||||||
)
|
|
||||||
from dgsi.models import Archive, Bylaws, Service, Statutes, User
|
|
||||||
from shared.kanidm import klient, sync_call
|
|
||||||
|
|
||||||
|
|
||||||
class Link(NamedTuple):
|
|
||||||
color: str
|
|
||||||
reverse: str
|
|
||||||
text: str | Promise
|
|
||||||
icon: str | None = None
|
|
||||||
absolute: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
AUTHENTICATED_LINKS = [
|
|
||||||
Link("is-primary", "dgsi:profile", _("Mon profil"), "user-filled"),
|
|
||||||
Link(
|
|
||||||
"is-success",
|
|
||||||
"https://docs.dgnum.eu/s/doc-publique",
|
|
||||||
_("Aide et Documention"),
|
|
||||||
"help",
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
Link("is-primary", "dgsi:legal_documents", _("Documents Légaux"), "script"),
|
|
||||||
Link("is-info", "dgsi:service_list", _("Services proposés"), "apps-filled"),
|
|
||||||
Link("is-success", "dgsi:archive_list", _("Archives"), "archive"),
|
|
||||||
]
|
|
||||||
|
|
||||||
ADMIN_LINKS = [
|
|
||||||
Link(
|
|
||||||
"is-danger",
|
|
||||||
"dgsi:create_kanidm_account",
|
|
||||||
_("Créer un compte Kanidm"),
|
|
||||||
"user-plus",
|
|
||||||
),
|
|
||||||
Link("is-primary", "dgsi:user_list", _("Liste des comptes"), "users"),
|
|
||||||
Link(
|
|
||||||
"is-warning", "admin:index", _("Interface d'administration"), "settings-filled"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
|
||||||
template_name = "dgsi/index.html"
|
|
||||||
extra_context = {
|
|
||||||
"links": {
|
|
||||||
"authenticated": AUTHENTICATED_LINKS,
|
|
||||||
"admin": ADMIN_LINKS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(LoginRequiredMixin, TemplateView):
|
|
||||||
template_name = "dgsi/profile.html"
|
|
||||||
|
|
||||||
|
|
||||||
class AppleProfileView(KanidmAccountRequiredMixin, TemplateView):
|
|
||||||
content_type = "application/x-apple-aspen-config"
|
|
||||||
template_name = "dgnum_profile.mobileconfig"
|
|
||||||
extra_context = {"dgnum_ssid": "DGNum"}
|
|
||||||
require_radius_secret = True
|
|
||||||
|
|
||||||
def render_to_response(
|
|
||||||
self, context: dict[str, Any], **response_kwargs: Any
|
|
||||||
) -> HttpResponse:
|
|
||||||
headers = response_kwargs.pop("headers", {})
|
|
||||||
headers["Content-Disposition"] = "attachment; filename=wifi_dgnum.mobileconfig"
|
|
||||||
return super().render_to_response(context, headers=headers, **response_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateWiFiPasswordView(KanidmAccountRequiredMixin, View):
|
|
||||||
url = reverse_lazy("dgsi:profile")
|
|
||||||
http_method_names = ["post"]
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
|
||||||
assert self._user.kanidm is not None
|
|
||||||
|
|
||||||
# Give access to the wifi network when the user creates its first password
|
|
||||||
if not self._user.kanidm.radius_secret:
|
|
||||||
message = _("Mot de passe Wi-Fi généré avec succès.")
|
|
||||||
sync_call("group_add_members", "radius_access", [self._user.username])
|
|
||||||
else:
|
|
||||||
message = _("Mot de passe Wi-Fi reinitialisé avec succès.")
|
|
||||||
sync_call("call_post", f"/v1/person/{self._user.username}/_radius")
|
|
||||||
messages.add_message(request, messages.SUCCESS, message)
|
|
||||||
|
|
||||||
return HttpResponse(*args, headers={"HX-Redirect": self.url}, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to
|
|
||||||
# use dispatch means that we need to execute the login check anyways.
|
|
||||||
class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView):
|
|
||||||
template_name = "dgsi/create_self_account.html"
|
|
||||||
form_class = CreateSelfAccountForm
|
|
||||||
success_message = _("Compte DGNum créé avec succès")
|
|
||||||
success_url = reverse_lazy("dgsi:profile")
|
|
||||||
extra_context = {"backlink": "dgsi:profile"}
|
|
||||||
|
|
||||||
def dispatch(
|
|
||||||
self, request: HttpRequest, *args: Any, **kwargs: Any
|
|
||||||
) -> HttpResponseBase:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
|
|
||||||
u = User.from_request(request)
|
|
||||||
|
|
||||||
# Check that the user does not already exist
|
|
||||||
if u.kanidm is not None:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_("<b>Vous possédez déjà un compte DGNum !</b>"),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy("dgsi:profile"))
|
|
||||||
|
|
||||||
# Check that the Statutes and Bylaws have been accepted
|
|
||||||
if (
|
|
||||||
u.accepted_statutes != Statutes.latest()
|
|
||||||
or u.accepted_bylaws != Bylaws.latest()
|
|
||||||
):
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_("Vous devez accepter les Statuts et le Règlement Intérieur."),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy("dgsi:legal_documents"))
|
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
@async_to_sync
|
|
||||||
async def form_valid(self, form):
|
|
||||||
ttl = 86400 # 24h
|
|
||||||
|
|
||||||
d = form.cleaned_data
|
|
||||||
u = User.from_request(self.request)
|
|
||||||
|
|
||||||
# Create the base account
|
|
||||||
await klient.person_account_create(u.username, d["displayname"])
|
|
||||||
|
|
||||||
# Update the information
|
|
||||||
await klient.person_account_update(u.username, mail=[d["mail"]])
|
|
||||||
|
|
||||||
# FIXME: Will maybe change when kanidm gets its shit together and switches to POST
|
|
||||||
r = await klient.call_get(
|
|
||||||
f"/v1/person/{u.username}/_credential/_update_intent/{ttl}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert r.data is not None
|
|
||||||
|
|
||||||
token: str = r.data["token"]
|
|
||||||
link = f"https://sso.dgnum.eu/ui/reset?token={token}"
|
|
||||||
|
|
||||||
# Send an email to the new user with the given email address
|
|
||||||
EmailMessage(
|
|
||||||
subject="Réinitialisation de mot de passe DGNum -- DGNum password reset",
|
|
||||||
body=render_to_string(
|
|
||||||
"mail/credentials_reset.txt",
|
|
||||||
context={"link": link},
|
|
||||||
),
|
|
||||||
from_email="To Be Determined <dgsi@infra.dgnum.eu>",
|
|
||||||
to=[d["mail"]],
|
|
||||||
headers={"Reply-To": "contact@dgnum.eu"},
|
|
||||||
).send()
|
|
||||||
|
|
||||||
if settings.VLAN_AUTOCONNECT:
|
|
||||||
u.register_unique_vlan()
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class ArchiveListView(LoginRequiredMixin, TemplateView):
|
|
||||||
template_name = "dgsi/archive_list.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
return super().get_context_data(
|
|
||||||
document_list=sorted(
|
|
||||||
[
|
|
||||||
*Archive.objects.all(),
|
|
||||||
*Bylaws.objects.all(),
|
|
||||||
*Statutes.objects.all(),
|
|
||||||
],
|
|
||||||
key=lambda obj: obj.date,
|
|
||||||
reverse=True,
|
|
||||||
),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProtectedArchiveView(LoginRequiredMixin, View):
|
|
||||||
http_method_names = ["get"]
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
|
||||||
u = User.from_request(request)
|
|
||||||
archive = Archive.objects.get(pk=self.kwargs["pk"])
|
|
||||||
|
|
||||||
if u.can_access_archive(archive):
|
|
||||||
# INFO: When in DEBUG mode, redirect to the "real" file
|
|
||||||
if settings.DEBUG:
|
|
||||||
return HttpResponseRedirect(redirect_to=archive.file.url)
|
|
||||||
|
|
||||||
content_type, encoding = guess_type(archive.file.name)
|
|
||||||
|
|
||||||
if encoding is not None:
|
|
||||||
content_type = {
|
|
||||||
"br": "application/x-brotli",
|
|
||||||
"bzip2": "application/x-bzip",
|
|
||||||
"compress": "application/x-compress",
|
|
||||||
"gzip": "application/gzip",
|
|
||||||
"xz": "application/x-xz",
|
|
||||||
}.get(encoding, content_type)
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
headers={
|
|
||||||
"Content-Type": content_type,
|
|
||||||
"Content-Disposition": f"inline; filename={archive.file.name}",
|
|
||||||
"X-Accel-Redirect": f"/{settings.ARCHIVES_INTERNAL}/{archive.file.name}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
|
|
||||||
class LegalDocumentsView(LoginRequiredMixin, TemplateView):
|
|
||||||
template_name = "dgsi/legal_documents.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
u = User.from_request(self.request)
|
|
||||||
statutes = Statutes.latest()
|
|
||||||
bylaws = Bylaws.latest()
|
|
||||||
|
|
||||||
return super().get_context_data(
|
|
||||||
statutes=statutes,
|
|
||||||
bylaws=bylaws,
|
|
||||||
show_message=(
|
|
||||||
(u.accepted_bylaws != bylaws) or (u.accepted_statutes != statutes)
|
|
||||||
),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AcceptLegalDocumentView(LoginRequiredMixin, RedirectView):
|
|
||||||
url = reverse_lazy("dgsi:legal_documents")
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
|
||||||
u = User.from_request(self.request)
|
|
||||||
|
|
||||||
match kwargs.get("kind"):
|
|
||||||
case "statutes":
|
|
||||||
u.accepted_statutes = Statutes.latest()
|
|
||||||
u.save()
|
|
||||||
case "bylaws":
|
|
||||||
u.accepted_bylaws = Bylaws.latest()
|
|
||||||
u.save()
|
|
||||||
case k:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_("Type de document invalide : %(kind)s") % {"kind": k},
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# INFO: Below are classes related to services offered by the DGNum
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceListView(LoginRequiredMixin, ListView):
|
|
||||||
model = Service
|
|
||||||
|
|
||||||
# TODO: Only show available websites
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceRedirectView(LoginRequiredMixin, SingleObjectMixin, RedirectView):
|
|
||||||
model = Service
|
|
||||||
|
|
||||||
def get_redirect_url(self, *args: Any, **kwargs: Any) -> str:
|
|
||||||
return self.get_object().url
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# INFO: Below are views related to the administration of DGSI
|
|
||||||
|
|
||||||
|
|
||||||
class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView):
|
|
||||||
form_class = CreateKanidmAccountForm
|
|
||||||
template_name = "dgsi/create_kanidm_account.html"
|
|
||||||
success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.")
|
|
||||||
|
|
||||||
success_url = reverse_lazy("dgsi:create_kanidm_user")
|
|
||||||
|
|
||||||
@async_to_sync
|
|
||||||
async def form_valid(self, form):
|
|
||||||
ttl = 86400 # 24h
|
|
||||||
|
|
||||||
d = form.cleaned_data
|
|
||||||
|
|
||||||
# Create the base account
|
|
||||||
await klient.person_account_create(d["name"], d["displayname"])
|
|
||||||
|
|
||||||
# Update the information
|
|
||||||
await klient.person_account_update(d["name"], mail=[d["mail"]])
|
|
||||||
|
|
||||||
# If necessary, add the user to the active members group
|
|
||||||
if d["active"]:
|
|
||||||
await klient.group_add_members("dgnum_members", [d["name"]])
|
|
||||||
|
|
||||||
# FIXME: Will maybe change when kanidm gets its shit together and switches to POST
|
|
||||||
r = await klient.call_get(
|
|
||||||
f"/v1/person/{d['name']}/_credential/_update_intent/{ttl}"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert r.data is not None
|
|
||||||
|
|
||||||
token: str = r.data["token"]
|
|
||||||
link = f"https://sso.dgnum.eu/ui/reset?token={token}"
|
|
||||||
|
|
||||||
# Send an email to the new user with the given email address
|
|
||||||
EmailMessage(
|
|
||||||
subject="Réinitialisation de mot de passe DGNum -- DGNum password reset",
|
|
||||||
body=render_to_string(
|
|
||||||
"mail/credentials_reset.txt",
|
|
||||||
context={"link": link},
|
|
||||||
),
|
|
||||||
to=[d["mail"]],
|
|
||||||
headers={"Reply-To": "contact@dgnum.eu"},
|
|
||||||
).send()
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class UserListView(StaffRequiredMixin, ListView):
|
|
||||||
model = User
|
|
||||||
ordering = ["-date_joined"]
|
|
||||||
|
|
||||||
|
|
||||||
class UserAssignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
|
|
||||||
model = User
|
|
||||||
template_name = "dgsi/partials/user_list-user.html"
|
|
||||||
context_object_name = "person"
|
|
||||||
|
|
||||||
def execute_action(self, *args, **kwargs) -> None:
|
|
||||||
self.object.register_unique_vlan()
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeassignVlanView(StaffRequiredMixin, HtmxPostObjectMixin, View):
|
|
||||||
model = User
|
|
||||||
template_name = "dgsi/partials/user_list-user.html"
|
|
||||||
context_object_name = "person"
|
|
||||||
|
|
||||||
def execute_action(self, *args, **kwargs) -> None:
|
|
||||||
self.object.reclaim_vlan()
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
import logging
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from allauth.core.exceptions import ImmediateHttpResponse
|
|
||||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
|
||||||
from allauth.socialaccount.models import SocialLogin
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.http import HttpRequest, HttpResponseRedirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from dgsi.models import Translation, User
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
|
||||||
"""
|
|
||||||
Overrides the Account Adapter, to allow a simpler connection via CAS.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def _get_username(self, request: HttpRequest, sociallogin: SocialLogin) -> str:
|
|
||||||
"""
|
|
||||||
Returns the required username
|
|
||||||
"""
|
|
||||||
|
|
||||||
match sociallogin.account.provider:
|
|
||||||
case "ens_cas":
|
|
||||||
cas_login = sociallogin.account.extra_data["uid"]
|
|
||||||
|
|
||||||
# Verify that this user can indeed connect to the website
|
|
||||||
home = sociallogin.account.extra_data["homeDirectory"].split("/")
|
|
||||||
|
|
||||||
if (home[1] != "users") or (
|
|
||||||
home[2]
|
|
||||||
in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"]
|
|
||||||
):
|
|
||||||
messages.error(request, _("Catégorie de compte ENS interdite."))
|
|
||||||
raise ImmediateHttpResponse(
|
|
||||||
HttpResponseRedirect(reverse("dgsi:forbidden_account"))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Continue with the login flow
|
|
||||||
try:
|
|
||||||
return Translation.objects.get(cas_login=cas_login).username
|
|
||||||
except Translation.DoesNotExist:
|
|
||||||
return cas_login
|
|
||||||
|
|
||||||
case "kanidm":
|
|
||||||
return sociallogin.account.extra_data["preferred_username"]
|
|
||||||
|
|
||||||
case _:
|
|
||||||
logger.warning(sociallogin.user)
|
|
||||||
# INFO: This should never happen
|
|
||||||
messages.error(request, _("Méthode de connexion invalide."))
|
|
||||||
raise ImmediateHttpResponse(
|
|
||||||
HttpResponseRedirect(reverse("dgsi:forbidden_account"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_user(
|
|
||||||
self, request: HttpRequest, sociallogin: SocialLogin
|
|
||||||
) -> Optional[User]:
|
|
||||||
"""
|
|
||||||
Returns the required user for completing the login
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The user is already linked to the social login, no reason to change it
|
|
||||||
if sociallogin.is_existing:
|
|
||||||
return sociallogin.user
|
|
||||||
|
|
||||||
# No user is currently linked to this social login, either the user has already
|
|
||||||
# logged in with another method, or it truly does not exist
|
|
||||||
return User.objects.filter(
|
|
||||||
username=self._get_username(request, sociallogin)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def _update_user(self, request: HttpRequest, sociallogin: SocialLogin):
|
|
||||||
"""
|
|
||||||
Updates the username of the user.
|
|
||||||
|
|
||||||
There are two usecases for this:
|
|
||||||
|
|
||||||
- Choosing the correct username at the first login:
|
|
||||||
+ Get the `preferred_username` for the DGNum login
|
|
||||||
+ Get the `cas_login` for the CAS login
|
|
||||||
|
|
||||||
- Updating the username when it is changed remotely
|
|
||||||
+ kanidm allows updating the `preferred_username`
|
|
||||||
+ a translation can be added for CAS logins
|
|
||||||
"""
|
|
||||||
|
|
||||||
u = sociallogin.user
|
|
||||||
|
|
||||||
assert isinstance(u, User)
|
|
||||||
|
|
||||||
# Update the username first, so that calls to kanidm return the correct information
|
|
||||||
u.username = self._get_username(request, sociallogin)
|
|
||||||
|
|
||||||
# Save the updated user if needed
|
|
||||||
if sociallogin.is_existing:
|
|
||||||
u.save()
|
|
||||||
|
|
||||||
def pre_social_login(self, request, sociallogin: SocialLogin):
|
|
||||||
###
|
|
||||||
# The flow is the following:
|
|
||||||
# - Get the correct user
|
|
||||||
# - Do the connection if possible
|
|
||||||
# - Update the required attributes
|
|
||||||
|
|
||||||
user = self._get_user(request, sociallogin)
|
|
||||||
|
|
||||||
if user is not None:
|
|
||||||
sociallogin.user = user
|
|
||||||
|
|
||||||
# If the user exists, connect to it
|
|
||||||
if sociallogin.is_existing:
|
|
||||||
sociallogin.connect(request, sociallogin.user)
|
|
||||||
|
|
||||||
self._update_user(request, sociallogin)
|
|
||||||
|
|
||||||
def populate_user(self, request, sociallogin, data):
|
|
||||||
return super().populate_user(request, sociallogin, data)
|
|
||||||
|
|
||||||
def save_user(self, request, sociallogin: SocialLogin, form=None):
|
|
||||||
return super().save_user(request, sociallogin, form)
|
|
|
@ -1,14 +0,0 @@
|
||||||
from import_export.admin import (
|
|
||||||
ImportExportMixin,
|
|
||||||
ImportForm,
|
|
||||||
SelectableFieldsExportForm,
|
|
||||||
)
|
|
||||||
from import_export.forms import ExportForm
|
|
||||||
from unfold.admin import ModelAdmin
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModelAdmin(ImportExportMixin, ModelAdmin):
|
|
||||||
compressed_fields = True
|
|
||||||
import_form_class = ImportForm
|
|
||||||
export_form_class = ExportForm
|
|
||||||
export_form_class = SelectableFieldsExportForm
|
|
|
@ -1,11 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class LinkLoginForm(forms.Form):
|
|
||||||
email = forms.EmailField(
|
|
||||||
label=_("Adresse e-mail"),
|
|
||||||
help_text=_(
|
|
||||||
"Si un compte correspondant à cette adresse e-mail est trouvée, un lien de connexion y sera envoyé."
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,26 +0,0 @@
|
||||||
from django.contrib.auth import user_logged_in
|
|
||||||
from django.dispatch.dispatcher import receiver
|
|
||||||
|
|
||||||
from app import settings
|
|
||||||
from dgsi.models import User
|
|
||||||
from shared.kanidm import sync_call
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: We use a signal to update permissions so that using magic links
|
|
||||||
# Two kind of users exist, regular `User`s and `ServiceUser`s.
|
|
||||||
# Updating permissions should only be done for regular ones.
|
|
||||||
@receiver(user_logged_in, sender=User)
|
|
||||||
def update_userinfo(**kwargs):
|
|
||||||
u = kwargs["user"]
|
|
||||||
|
|
||||||
# Update the global permissions
|
|
||||||
u.is_superuser = u.part_of(settings.DGSI_SUPERUSER_GROUP)
|
|
||||||
u.is_staff = u.is_superuser or u.part_of(settings.DGSI_STAFF_GROUP)
|
|
||||||
|
|
||||||
# Update the e-mail address if possible
|
|
||||||
if u.kanidm is not None:
|
|
||||||
emails = sync_call("call_get", f"/v1/person/{u.username}/_attr/mail").data
|
|
||||||
if emails != []:
|
|
||||||
u.email = emails[0]
|
|
||||||
|
|
||||||
u.save()
|
|
|
@ -1,10 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = "authentication"
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", views.LoginFormView.as_view(), name="link_login_form"),
|
|
||||||
path("<str:token>/", views.LoginView.as_view(), name="link_login"),
|
|
||||||
]
|
|
|
@ -1,109 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth import authenticate, login
|
|
||||||
from django.contrib.auth.views import RedirectURLMixin
|
|
||||||
from django.core.mail import EmailMessage
|
|
||||||
from django.http import Http404
|
|
||||||
from django.http.response import HttpResponse, HttpResponseRedirect
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.urls import reverse, reverse_lazy
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views import View
|
|
||||||
from django.views.generic import FormView
|
|
||||||
from sesame.utils import get_token
|
|
||||||
|
|
||||||
from dgsi.models import User
|
|
||||||
from shared.authentication.forms import LinkLoginForm
|
|
||||||
|
|
||||||
|
|
||||||
class LoginFormView(FormView):
|
|
||||||
template_name = "account/link_form.html"
|
|
||||||
form_class = LinkLoginForm
|
|
||||||
success_url = reverse_lazy("account_login")
|
|
||||||
extra_context = {"backlink": "account_login"}
|
|
||||||
|
|
||||||
def form_valid(self, form) -> HttpResponse:
|
|
||||||
u = User.objects.filter(email=form.cleaned_data["email"]).first()
|
|
||||||
|
|
||||||
if u is not None:
|
|
||||||
# Send an e-mail containing the token
|
|
||||||
link = self.request.build_absolute_uri(
|
|
||||||
reverse("authentication:link_login", kwargs={"token": get_token(u)})
|
|
||||||
)
|
|
||||||
|
|
||||||
EmailMessage(
|
|
||||||
subject="Connexion au profil DGNum -- DGNum profile login",
|
|
||||||
body=render_to_string(
|
|
||||||
"mail/magic_link.txt",
|
|
||||||
context={"link": link},
|
|
||||||
),
|
|
||||||
from_email="To Be Determined <dgsi@infra.dgnum.eu>",
|
|
||||||
to=[u.email],
|
|
||||||
headers={"Reply-To": "contact@dgnum.eu"},
|
|
||||||
).send()
|
|
||||||
|
|
||||||
messages.add_message(
|
|
||||||
self.request,
|
|
||||||
messages.INFO,
|
|
||||||
_(
|
|
||||||
"Un e-mail a été envoyé à l'adresse renseignée si un compte y étant associé a été trouvé."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginView(RedirectURLMixin, View):
|
|
||||||
"""
|
|
||||||
Look for a signed token in the URL of a GET request and log a user in.
|
|
||||||
|
|
||||||
If a valid token is found, the user is redirected to the URL specified in
|
|
||||||
the ``next`` query string parameter or the ``next_page`` attribute of the
|
|
||||||
view. ``next_page`` defaults to :setting:`LOGIN_REDIRECT_URL`.
|
|
||||||
|
|
||||||
If a ``scope`` attribute is set, a :ref:`scoped token <Scoped tokens>` is
|
|
||||||
expected.
|
|
||||||
|
|
||||||
If a ``max_age`` attribute is set, override the :data:`SESAME_MAX_AGE`
|
|
||||||
setting.
|
|
||||||
|
|
||||||
In addition to ``next_page``, :class:`LoginView` also supports
|
|
||||||
``redirect_field_name``, ``success_url_allowed_hosts``, and
|
|
||||||
``get_default_redirect_url()``. These APIs behave like their counterparts
|
|
||||||
in Django's built-in :class:`~django.contrib.auth.views.LoginView`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
scope = ""
|
|
||||||
max_age = None
|
|
||||||
next_page = settings.LOGIN_REDIRECT_URL
|
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
|
||||||
token = kwargs.get("token")
|
|
||||||
|
|
||||||
if token is None:
|
|
||||||
return self.login_failed()
|
|
||||||
|
|
||||||
user = authenticate(
|
|
||||||
request,
|
|
||||||
sesame=token,
|
|
||||||
scope=self.scope,
|
|
||||||
max_age=self.max_age,
|
|
||||||
)
|
|
||||||
if user is None:
|
|
||||||
return self.login_failed()
|
|
||||||
|
|
||||||
login(request, user) # updates the last login date
|
|
||||||
|
|
||||||
return self.login_success()
|
|
||||||
|
|
||||||
def login_failed(self):
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
def login_success(self):
|
|
||||||
if self.next_page is None:
|
|
||||||
return HttpResponse(status=HTTPStatus.NO_CONTENT)
|
|
||||||
else:
|
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
|
|
@ -1,11 +0,0 @@
|
||||||
from allauth.socialaccount.providers.base import ProviderAccount
|
|
||||||
from allauth_cas.providers import CASProvider as Provider
|
|
||||||
|
|
||||||
|
|
||||||
class CASProvider(Provider):
|
|
||||||
id = "cas" # Choose an identifier for your provider
|
|
||||||
name = "CAS ENS" # Verbose name of your provider
|
|
||||||
account_class = ProviderAccount
|
|
||||||
|
|
||||||
|
|
||||||
provider_classes = [CASProvider]
|
|
|
@ -1,5 +0,0 @@
|
||||||
from allauth_cas.urls import default_urlpatterns
|
|
||||||
|
|
||||||
from .provider import CASProvider
|
|
||||||
|
|
||||||
urlpatterns = default_urlpatterns(CASProvider)
|
|
|
@ -1,14 +0,0 @@
|
||||||
from allauth_cas.views import CASAdapter as Adapter
|
|
||||||
from allauth_cas.views import CASCallbackView, CASLoginView
|
|
||||||
|
|
||||||
from .provider import CASProvider
|
|
||||||
|
|
||||||
|
|
||||||
class CASAdapter(Adapter):
|
|
||||||
provider_id = CASProvider.id
|
|
||||||
url = "https://cas.eleves.ens.fr"
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
|
|
||||||
login = CASLoginView.adapter_view(CASAdapter)
|
|
||||||
callback = CASCallbackView.adapter_view(CASAdapter)
|
|
|
@ -1,16 +0,0 @@
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from kanidm import KanidmClient
|
|
||||||
from loadcredential import Credentials
|
|
||||||
|
|
||||||
credentials = Credentials(env_prefix="DGSI_")
|
|
||||||
|
|
||||||
klient = KanidmClient(
|
|
||||||
uri=credentials["KANIDM_URI"], token=credentials["KANIDM_AUTH_TOKEN"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_call(name, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Wraps the required action for use in sync contexts
|
|
||||||
"""
|
|
||||||
return async_to_sync(getattr(klient, name))(*args, **kwargs)
|
|
Binary file not shown.
|
@ -1,416 +0,0 @@
|
||||||
# DG·SI english translation
|
|
||||||
# Copyright (C) 2024 DGNum
|
|
||||||
# This file is distributed under the same license as the dgsi package.
|
|
||||||
# Tom Hubrecht <tom.hubrecht@dgnum.eu>, 2024-2025.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: dgsi.dgnum.eu\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2025-05-03 16:02+0200\n"
|
|
||||||
"PO-Revision-Date: 2025-05-03 16:05+0200\n"
|
|
||||||
"Last-Translator: Tom Hubrecht <tom.hubrecht@dgnum.eu>\n"
|
|
||||||
"Language-Team: French\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
|
||||||
"X-Generator: Gtranslator 48.0\n"
|
|
||||||
|
|
||||||
msgid "Permissions"
|
|
||||||
msgstr "Permissions"
|
|
||||||
|
|
||||||
msgid "groups"
|
|
||||||
msgstr "groups"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"The groups this user belongs to. A user will get all permissions granted to "
|
|
||||||
"each of their groups."
|
|
||||||
msgstr ""
|
|
||||||
"The groups this user belongs to. A user will get all permissions granted to "
|
|
||||||
"each of their groups."
|
|
||||||
|
|
||||||
msgid "user permissions"
|
|
||||||
msgstr "user permissions"
|
|
||||||
|
|
||||||
msgid "Specific permissions for this user."
|
|
||||||
msgstr "Specific permissions for this user."
|
|
||||||
|
|
||||||
msgid "Service user"
|
|
||||||
msgstr "Services user"
|
|
||||||
|
|
||||||
msgid "Service users"
|
|
||||||
msgstr "Services users"
|
|
||||||
|
|
||||||
msgid "Key"
|
|
||||||
msgstr "Key"
|
|
||||||
|
|
||||||
msgid "Administration de DGSI"
|
|
||||||
msgstr "DGSI Administration"
|
|
||||||
|
|
||||||
msgid "Informations réseau"
|
|
||||||
msgstr "Networking informations"
|
|
||||||
|
|
||||||
msgid "Documents DGNum"
|
|
||||||
msgstr "DGNum Documents"
|
|
||||||
|
|
||||||
msgid "Identifiant déjà présent dans la base de données."
|
|
||||||
msgstr "Username already in the database."
|
|
||||||
|
|
||||||
msgid "Identifiant"
|
|
||||||
msgstr "Username"
|
|
||||||
|
|
||||||
msgid "De préférence identique au login ENS de la personne concernée."
|
|
||||||
msgstr "Preferably identical to the ENS login of the person concerned."
|
|
||||||
|
|
||||||
msgid "Nom d'usage"
|
|
||||||
msgstr "Name in use"
|
|
||||||
|
|
||||||
msgid "Adresse e-mail"
|
|
||||||
msgstr "E-mail address"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"De préférence :<br>- l'adresse <code>@ens.psl.eu</code> pour les personnes "
|
|
||||||
"en scolarité ;<br>- l'adresse <code>@normalesup.org</code> pour les "
|
|
||||||
"personnes ayant fini leur scolarité ;<br><b>Pour les personnes extérieures, "
|
|
||||||
"le bureau doit donner son accord.</b>"
|
|
||||||
msgstr ""
|
|
||||||
"Preferably:<br>- the <code>@ens.psl.eu</code> address for students;<br>- the "
|
|
||||||
"<code>@normalesup.org</code> address for people having finished their "
|
|
||||||
"studies;<br><b>For outsiders, the board must give its approval.</b>"
|
|
||||||
|
|
||||||
msgid "Membre actif"
|
|
||||||
msgstr "Active member"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Si selectionné, la personne sera ajoutée au groupe <code>dgnum_members</"
|
|
||||||
"code>.<br><b>L'accord préalable du bureau est nécessaire !</b>"
|
|
||||||
msgstr ""
|
|
||||||
"If selected, the person will be added to the <code>dgnum_members</code> "
|
|
||||||
"group.<br><b>Prior approval from the board is required!</b>"
|
|
||||||
|
|
||||||
msgid "De préférence l'adresse '@ens.psl.eu'"
|
|
||||||
msgstr "Preferably the ‘@ens.psl.eu’ address"
|
|
||||||
|
|
||||||
msgid "<b>Veuillez créer un compte DGNum.</b>"
|
|
||||||
msgstr "<b>Please create a DGNum account.</b>"
|
|
||||||
|
|
||||||
msgid "<b>Veuillez générer un mot de passe Wi-Fi.</b>"
|
|
||||||
msgstr "<b>Please generate a WiFi password.</b>"
|
|
||||||
|
|
||||||
msgid "Nom du service proposé"
|
|
||||||
msgstr "Name of the proposed service"
|
|
||||||
|
|
||||||
msgid "Adresse du service"
|
|
||||||
msgstr "Address of the service"
|
|
||||||
|
|
||||||
msgid "Icône du service"
|
|
||||||
msgstr "Icon of the service"
|
|
||||||
|
|
||||||
msgid "Date du document"
|
|
||||||
msgstr "Document date"
|
|
||||||
|
|
||||||
msgid "Nom du document"
|
|
||||||
msgstr "Document name"
|
|
||||||
|
|
||||||
msgid "Fichier PDF"
|
|
||||||
msgstr "PDF file"
|
|
||||||
|
|
||||||
msgid "Statuts"
|
|
||||||
msgstr "Statutes"
|
|
||||||
|
|
||||||
msgid "Règlement Intérieur"
|
|
||||||
msgstr "Bylaws"
|
|
||||||
|
|
||||||
msgid "Règlements Intérieurs"
|
|
||||||
msgstr "Bylaws"
|
|
||||||
|
|
||||||
msgid "Document d'archives"
|
|
||||||
msgstr "Archive document"
|
|
||||||
|
|
||||||
msgid "Documents d'archives"
|
|
||||||
msgstr "Archive documents"
|
|
||||||
|
|
||||||
msgid "Correspondance de login"
|
|
||||||
msgstr "Login mapping"
|
|
||||||
|
|
||||||
msgid "Correspondances de login"
|
|
||||||
msgstr "Login mappings"
|
|
||||||
|
|
||||||
msgid "Derniers Statuts acceptés"
|
|
||||||
msgstr "Latest accepted Statutes"
|
|
||||||
|
|
||||||
msgid "Dernier Règlement Intérieur accepté"
|
|
||||||
msgstr "Latest accepted Bylaws"
|
|
||||||
|
|
||||||
msgid "VLAN associé au compte"
|
|
||||||
msgstr "VLAN assigned to the account"
|
|
||||||
|
|
||||||
msgid "Ce compte a déjà un VLAN associé"
|
|
||||||
msgstr "This account already has an assigned VLAN"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
" En acceptant, vous assurez avoir lu ce document et en approuver le contenu."
|
|
||||||
msgstr ""
|
|
||||||
" By accepting, you confirm that you have read this document and agree with "
|
|
||||||
"its content."
|
|
||||||
|
|
||||||
msgid "Accepté"
|
|
||||||
msgstr "Accepted"
|
|
||||||
|
|
||||||
msgid "Retour"
|
|
||||||
msgstr "Go back"
|
|
||||||
|
|
||||||
msgid "Archives de la DGNum"
|
|
||||||
msgstr "Archives of the DGNum"
|
|
||||||
|
|
||||||
msgid "Création de compte Kanidm"
|
|
||||||
msgstr "Kanidm account creation"
|
|
||||||
|
|
||||||
msgid "Enregistrer"
|
|
||||||
msgstr "Save"
|
|
||||||
|
|
||||||
msgid "Création d'un compte DGNum"
|
|
||||||
msgstr "DGNum account creation"
|
|
||||||
|
|
||||||
msgid "Documents légaux"
|
|
||||||
msgstr "Legal documents"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant "
|
|
||||||
"de pouvoir créer un compte."
|
|
||||||
msgstr ""
|
|
||||||
"You must accept the DGNum Statutes and Bylaws before you can create an "
|
|
||||||
"account."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer "
|
|
||||||
"un."
|
|
||||||
msgstr "You do not yet have a DGNum account, but you can now create one."
|
|
||||||
|
|
||||||
msgid "Poursuivre la création d'un compte DGNum"
|
|
||||||
msgstr "Continue the creation of a DGNum account"
|
|
||||||
|
|
||||||
msgid "Accepter les statuts"
|
|
||||||
msgstr "Accept the statutes"
|
|
||||||
|
|
||||||
msgid "Accepter le règlement intérieur"
|
|
||||||
msgstr "Accept the bylaws"
|
|
||||||
|
|
||||||
msgid "Mot de passe WiFi :"
|
|
||||||
msgstr "WiFi password:"
|
|
||||||
|
|
||||||
msgid "Êtes-vous sûr·e de vouloir réinitialiser votre mot de passe WiFi ?"
|
|
||||||
msgstr "Are you sure that you want to reset your WiFi password?"
|
|
||||||
|
|
||||||
msgid "Réinitialiser le mot de passe WiFi"
|
|
||||||
msgstr "Reset the WiFi password"
|
|
||||||
|
|
||||||
msgid "Générer un mot de passe WiFi"
|
|
||||||
msgstr "Generate a WiFi password:"
|
|
||||||
|
|
||||||
msgid "Désallouer"
|
|
||||||
msgstr "Deassign"
|
|
||||||
|
|
||||||
msgid "Allouer"
|
|
||||||
msgstr "Assign"
|
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Liste des comptes"
|
|
||||||
msgid "Pas de compte"
|
|
||||||
msgstr "List of accounts"
|
|
||||||
|
|
||||||
msgid "Profil personnel"
|
|
||||||
msgstr "Personal profile"
|
|
||||||
|
|
||||||
msgid "Nom d'utilisateur :"
|
|
||||||
msgstr "Username :"
|
|
||||||
|
|
||||||
msgid "Nom d'usage :"
|
|
||||||
msgstr "Name in use:"
|
|
||||||
|
|
||||||
msgid "Adresse e-mail :"
|
|
||||||
msgstr "E-mail address:"
|
|
||||||
|
|
||||||
msgid "Télécharger le profil Wi-Fi DGNum pour iOS, iPadOS et macOS"
|
|
||||||
msgstr "Download the DGNum Wi-Fi profile for iOS, iPadOS or macOS"
|
|
||||||
|
|
||||||
msgid "Informations techniques"
|
|
||||||
msgstr "Technical informations"
|
|
||||||
|
|
||||||
msgid "Afficher"
|
|
||||||
msgstr "Show"
|
|
||||||
|
|
||||||
msgid "Cacher"
|
|
||||||
msgstr "Hide"
|
|
||||||
|
|
||||||
msgid "Identifiant interne :"
|
|
||||||
msgstr "Internal identifier:"
|
|
||||||
|
|
||||||
msgid "VLAN attribué :"
|
|
||||||
msgstr "Assigned VLAN:"
|
|
||||||
|
|
||||||
msgid "Pas de VLAN attribué."
|
|
||||||
msgstr "No assigned VLAN."
|
|
||||||
|
|
||||||
msgid "Membre des groupes suivants :"
|
|
||||||
msgstr "Member of the following groups:"
|
|
||||||
|
|
||||||
msgid "Pas de compte DGNum répertorié."
|
|
||||||
msgstr "No DGNum account found."
|
|
||||||
|
|
||||||
msgid "Créer un compte DGNum"
|
|
||||||
msgstr "Create a DGNum account"
|
|
||||||
|
|
||||||
msgid "Services accessibles via la DGNum"
|
|
||||||
msgstr "Services accessible via the DGNum"
|
|
||||||
|
|
||||||
msgid "Nom d'utilisateur"
|
|
||||||
msgstr "Username"
|
|
||||||
|
|
||||||
msgid "VLAN attribué"
|
|
||||||
msgstr "Assigned VLAN"
|
|
||||||
|
|
||||||
msgid "Gestion du VLAN"
|
|
||||||
msgstr "VLAN management"
|
|
||||||
|
|
||||||
msgid "Mon profil"
|
|
||||||
msgstr "My profile"
|
|
||||||
|
|
||||||
msgid "Aide et Documention"
|
|
||||||
msgstr "Help and documentation"
|
|
||||||
|
|
||||||
msgid "Documents Légaux"
|
|
||||||
msgstr "Legal Documents"
|
|
||||||
|
|
||||||
msgid "Services proposés"
|
|
||||||
msgstr "Services offered"
|
|
||||||
|
|
||||||
msgid "Archives"
|
|
||||||
msgstr "Archives"
|
|
||||||
|
|
||||||
msgid "Créer un compte Kanidm"
|
|
||||||
msgstr "Create a Kanidm account"
|
|
||||||
|
|
||||||
msgid "Liste des comptes"
|
|
||||||
msgstr "List of accounts"
|
|
||||||
|
|
||||||
msgid "Interface d'administration"
|
|
||||||
msgstr "Administration interface"
|
|
||||||
|
|
||||||
msgid "Mot de passe Wi-Fi généré avec succès."
|
|
||||||
msgstr "Wi-Fi password generated successfully."
|
|
||||||
|
|
||||||
msgid "Mot de passe Wi-Fi reinitialisé avec succès."
|
|
||||||
msgstr "Wi-Fi password reset successfully."
|
|
||||||
|
|
||||||
msgid "Compte DGNum créé avec succès"
|
|
||||||
msgstr "DGNum account successfully created"
|
|
||||||
|
|
||||||
msgid "<b>Vous possédez déjà un compte DGNum !</b>"
|
|
||||||
msgstr "<b>You already have a DGNum account!</b>"
|
|
||||||
|
|
||||||
msgid "Vous devez accepter les Statuts et le Règlement Intérieur."
|
|
||||||
msgstr "You must accept the Statutes and the Bylaws."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Type de document invalide : %(kind)s"
|
|
||||||
msgstr "Invalid document type: %(kind)s"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Compte DGNum pour %(displayname)s [%(name)s] créé."
|
|
||||||
msgstr "DGNum account for %(displayname)s [%(name)s] created."
|
|
||||||
|
|
||||||
msgid "Catégorie de compte ENS interdite."
|
|
||||||
msgstr "ENS account category not permitted."
|
|
||||||
|
|
||||||
msgid "Méthode de connexion invalide."
|
|
||||||
msgstr "Invalid connection method."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Si un compte correspondant à cette adresse e-mail est trouvée, un lien de "
|
|
||||||
"connexion y sera envoyé."
|
|
||||||
msgstr ""
|
|
||||||
"If an account corresponding to this e-mail address is found, a connection "
|
|
||||||
"link will be sent to it."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Un e-mail a été envoyé à l'adresse renseignée si un compte y étant associé a "
|
|
||||||
"été trouvé."
|
|
||||||
msgstr ""
|
|
||||||
"An e-mail has been sent to the address entered if an account associated with "
|
|
||||||
"it has been found."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Logiciel développé pour et par la <a href=\"https://dgnum.eu\">DGNum</a>."
|
|
||||||
msgstr ""
|
|
||||||
"Software developed for and by the <a href=\"https://dgnum.eu\">DGNum</a>."
|
|
||||||
|
|
||||||
msgid "Déconnexion"
|
|
||||||
msgstr "Logout"
|
|
||||||
|
|
||||||
msgid "Connexion"
|
|
||||||
msgstr "Login"
|
|
||||||
|
|
||||||
msgid "Choix de la langue"
|
|
||||||
msgstr "Language selection"
|
|
||||||
|
|
||||||
msgid "Connexion par e-mail"
|
|
||||||
msgstr "Login via e-mail"
|
|
||||||
|
|
||||||
msgid "Envoyer un lien"
|
|
||||||
msgstr "Send a link"
|
|
||||||
|
|
||||||
msgid "Connexion via un compte tiers"
|
|
||||||
msgstr "Connection via a third-party account"
|
|
||||||
|
|
||||||
msgid "Êtes vous certain·e de vouloir vous déconnecter ?"
|
|
||||||
msgstr "Are you sure you want to log out?"
|
|
||||||
|
|
||||||
msgid "Se déconnecter"
|
|
||||||
msgstr "Log out"
|
|
||||||
|
|
||||||
msgid "Connexion impossible"
|
|
||||||
msgstr "Unable to connect"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Vos informations ne permettent pas de vous identifier auprès de la DGNum."
|
|
||||||
"<br>Si vous pensez qu'il s'agit une erreur, merci de nous contacter à "
|
|
||||||
"l'adresse : <a href=\"mailto:contact@dgnum.eu\">contact@dgnum.eu</a>"
|
|
||||||
msgstr ""
|
|
||||||
"Your details do not allow the DGNum to authenticate you.<br>If you think "
|
|
||||||
"this is a mistake, please contact us at: <a href=\"mailto:contact@dgnum."
|
|
||||||
"eu\">contact@dgnum.eu</a>"
|
|
||||||
|
|
||||||
msgid "Erreur lors de la connexion"
|
|
||||||
msgstr "Error during login"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Une erreur est survenue lors de votre tentative de connexion avec un compte "
|
|
||||||
"tiers."
|
|
||||||
msgstr ""
|
|
||||||
"An error has occurred while trying to login with a third-party account."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Se connecter via un compte <b>%(provider)s</b>"
|
|
||||||
msgstr "Log in with a <b>%(provider)s</b> account"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Vous vous apprêtez à vous connecter à l'aide d'un compte tiers provenant de "
|
|
||||||
"%(provider)s."
|
|
||||||
msgstr "You are about to log in using a third-party account from %(provider)s."
|
|
||||||
|
|
||||||
msgid "Continuer"
|
|
||||||
msgstr "Continue"
|
|
||||||
|
|
||||||
#~ msgid "Le VLAN {} est déjà attribué."
|
|
||||||
#~ msgstr "The VLAN {} is already assigned."
|
|
||||||
|
|
||||||
#~ msgid "Compte DGNum inexistant."
|
|
||||||
#~ msgstr "No existing DGNum account."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
#~ msgid "Profil de %(displayname)s"
|
|
||||||
#~ msgstr "Profile of %(displayname)s"
|
|
90
src/shared/static/bulma/bulma.scss
vendored
90
src/shared/static/bulma/bulma.scss
vendored
|
@ -7,100 +7,12 @@ $dark: rgb(46, 46, 46);
|
||||||
@use "sass" with (
|
@use "sass" with (
|
||||||
$primary: $blue,
|
$primary: $blue,
|
||||||
$link: rgb(72, 95, 199),
|
$link: rgb(72, 95, 199),
|
||||||
$dark: $dark
|
$dark: $dark,
|
||||||
);
|
);
|
||||||
|
|
||||||
@use "./sass/utilities/mixins" as mx;
|
|
||||||
@use "./sass/utilities/initial-variables.scss" as iv;
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
&.is-centered {
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
vertical-align: middle !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis {
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt-archive {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: calc(0.5 * var(--bulma-block-spacing));
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt-link {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: calc(0.5 * var(--bulma-block-spacing));
|
|
||||||
font-size: 1.25rem;
|
|
||||||
|
|
||||||
&.is-light {
|
|
||||||
// Dark color for text
|
|
||||||
--bulma-color-l: var(--bulma-dark-l);
|
|
||||||
--bulma-color-l-delta: 0%;
|
|
||||||
color: hsl(
|
|
||||||
var(--bulma-dark-h),
|
|
||||||
var(--bulma-dark-s),
|
|
||||||
calc(var(--bulma-color-l) + var(--bulma-color-l-delta))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt-links {
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
@include mx.from(iv.$desktop) {
|
|
||||||
width: 47.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mx.until(iv.$desktop) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid.groups {
|
|
||||||
--bulma-grid-column-min: min(24rem, 100%);
|
|
||||||
|
|
||||||
.button > span {
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#notifications {
|
|
||||||
margin-left: -0.75rem;
|
|
||||||
margin-right: -0.75rem;
|
|
||||||
position: sticky;
|
|
||||||
display: block;
|
|
||||||
inset: 1.5rem;
|
|
||||||
z-index: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
margin-bottom: var(--bulma-block-spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown.is-fullwidth {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.dropdown-trigger,
|
|
||||||
.dropdown-menu {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
const init = ($node) => {
|
|
||||||
const q = (query, f) => ($node.querySelectorAll(query) || []).forEach(f);
|
|
||||||
|
|
||||||
q(".notification .delete", ($delete) => {
|
|
||||||
const $notification = $delete.parentNode;
|
|
||||||
const dismiss = () => $notification.remove();
|
|
||||||
|
|
||||||
$delete.addEventListener("click", dismiss);
|
|
||||||
setTimeout(dismiss, 15000);
|
|
||||||
});
|
|
||||||
|
|
||||||
q("[data-toggle]", ($toggle) => {
|
|
||||||
const target = $node.querySelector($toggle.dataset.target);
|
|
||||||
|
|
||||||
$toggle.addEventListener("click", () => {
|
|
||||||
if ($toggle.dataset.toggle === "on") {
|
|
||||||
$toggle.dataset.toggle = "off";
|
|
||||||
$toggle.innerHTML = $toggle.dataset.offHtml;
|
|
||||||
target.classList.remove($toggle.dataset.class);
|
|
||||||
} else {
|
|
||||||
$toggle.dataset.toggle = "on";
|
|
||||||
$toggle.innerHTML = $toggle.dataset.onHtml;
|
|
||||||
target.classList.add($toggle.dataset.class);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
q("[data-select]", ($input) => {
|
|
||||||
$input.addEventListener("focus", () => {
|
|
||||||
$input.select();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.body.addEventListener("htmx:load", (e) => {
|
|
||||||
init(e.detail.elt);
|
|
||||||
});
|
|
||||||
});
|
|
1
src/shared/static/js/htmx.min.js
vendored
1
src/shared/static/js/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,6 @@
|
||||||
{% load i18n django_browser_reload %}
|
{% load django_browser_reload %}
|
||||||
|
|
||||||
<footer class="footer has-text-centered">
|
<footer class="footer has-text-centered">
|
||||||
<b>{% blocktrans %}Logiciel développé pour et par la <a href="https://dgnum.eu">DGNum</a>.{% endblocktrans %}</b>
|
<b>Logiciel développé pour et par la <a href="https://dgnum.eu">DGNum</a>.</b>
|
||||||
<hr class="my-2">
|
|
||||||
<a class="tag is-medium" href="{% url "dgsi:mentions_legales" %}">Mentions Légales</a>
|
|
||||||
{% django_browser_reload_script %}
|
{% django_browser_reload_script %}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -1,80 +1,37 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<section class="hero {% if admin_view %}is-danger{% else %}is-primary{% endif %}">
|
<section class="hero is-dark is-primary">
|
||||||
<div class="hero-body px-0">
|
<div class="hero-body">
|
||||||
<div class="columns mx-6">
|
<div class="container">
|
||||||
<div class="column is-three-quarters">
|
<div class="grid">
|
||||||
|
<div class="cell">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
<a href="{% url 'dgsi:index' %}" class="has-text-dark">Dossier Général des Services Informagiques</a>
|
<a href="{% url 'index' %}" class="has-text-dark">Dossier Général des Services Informagiques</a>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="subtitle mt-2">Système d'information de la DGNum</h2>
|
<h2 class="subtitle">Système d'information de la DGNum</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="cell">
|
||||||
<div class="buttons mt-5">
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="{% url 'account_logout' %}"
|
<a href="{% url 'account_logout' %}" class="button is-light is-pulled-right">
|
||||||
class="button is-light is-fullwidth">
|
|
||||||
<span>
|
<span>
|
||||||
|
<span>{% trans "Déconnexion" %}</span>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="ti ti-door-exit"></i>
|
<i class="ti ti-door-exit"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{% trans "Déconnexion" %}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'account_login' %}" class="button is-fullwidth is-light">
|
<a href="{% url 'login' %}" class="button is-light is-pulled-right">
|
||||||
<span>
|
<span>
|
||||||
|
<span>{% trans "Connexion" %}</span>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="ti ti-door-enter"></i>
|
<i class="ti ti-door-enter"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{% trans "Connexion" %}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="dropdown is-hoverable is-fullwidth">
|
|
||||||
<div class="dropdown-trigger"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-controls="lang-menu">
|
|
||||||
<button class="button is-primary is-light is-fullwidth has-text-dark">
|
|
||||||
<span class="icon"><i class="ti ti-language"></i></span>
|
|
||||||
<span>{% trans "Choix de la langue" %}</span>
|
|
||||||
<span class="icon is-pulled-right">
|
|
||||||
<i class="ti ti-chevron-down" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dropdown-menu" id="lang-menu" role="menu">
|
|
||||||
<div class="dropdown-content px-2">
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
|
||||||
{% get_available_languages as LANGUAGES %}
|
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
|
||||||
{% for language in languages %}
|
|
||||||
<form action="{% url 'set_language' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input name="next" type="hidden" value="{{ redirect }}">
|
|
||||||
<input type="hidden" name="language" value="{{ language.code }}">
|
|
||||||
<button class="dropdown-item {% if language.code == LANGUAGE_CODE %}is-active{% endif %}">
|
|
||||||
{{ language.name_local|capfirst }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if admin_view %}
|
|
||||||
<div class="hero is-dark">
|
|
||||||
<div class="hero-body py-2 has-text-centered">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="ti ti-lock-open-2"></i>
|
|
||||||
</span>
|
|
||||||
<span>{% trans "Interface d'administration" %}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,33 +1,12 @@
|
||||||
{% load django_htmx sass_tags static %}
|
{% load sass_tags static %}
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
<link href="{% static 'favicon.ico' %}" rel="icon" />
|
<link href="{% static 'favicon.ico' %}" rel="icon" />
|
||||||
<link href="{% static 'apple-touch-icon.png' %}" rel="apple-touch-icon" />
|
<link href="{% static 'apple-touch-icon.png' %}" rel="apple-touch-icon" />
|
||||||
<link rel="icon"
|
<link rel="icon" type="image/png" href="{% static 'favicon-16x16.png' %}" sizes="16x16" />
|
||||||
type="image/png"
|
<link rel="icon" type="image/png" href="{% static 'favicon-32x32.png' %}" sizes="32x32" />
|
||||||
href="{% static 'favicon-16x16.png' %}"
|
<link rel="icon" type="image/png" href="{% static 'android-chrome-192x192.png' %}" sizes="192x192" />
|
||||||
sizes="16x16" />
|
|
||||||
<link rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
href="{% static 'favicon-32x32.png' %}"
|
|
||||||
sizes="32x32" />
|
|
||||||
<link rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
href="{% static 'android-chrome-192x192.png' %}"
|
|
||||||
sizes="192x192" />
|
|
||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link href="{% sass_src 'bulma/bulma.scss' %}"
|
<link href="{% sass_src 'bulma/bulma.scss' %}" rel="stylesheet" type="text/css" />
|
||||||
rel="stylesheet"
|
<link href="{% static 'tabler-icons/tabler-icons.min.css' %}" rel="stylesheet" type="text/css" />
|
||||||
type="text/css" />
|
|
||||||
<link href="{% static 'tabler-icons/tabler-icons.min.css' %}"
|
|
||||||
rel="stylesheet"
|
|
||||||
type="text/css" />
|
|
||||||
|
|
||||||
<!-- JS -->
|
|
||||||
<script src="{% static 'js/dgsi.js' %}"></script>
|
|
||||||
<script defer src="{% static 'js/htmx.min.js' %}"></script>
|
|
||||||
<script defer
|
|
||||||
data-domain="profil.dgnum.eu"
|
|
||||||
src="https://analytics.dgnum.eu/js/script.js"></script>
|
|
||||||
{% django_htmx_script %}
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% trans "Connexion par e-mail" as subtitle %}
|
|
||||||
{% include "_subtitle.html" %}
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% include "bulma/form.html" %}
|
|
||||||
|
|
||||||
<button class="button is-fullwidth mt-6">
|
|
||||||
<span class="icon"><i class="ti ti-mail-share"></i></span>
|
|
||||||
<span>{% trans "Envoyer un lien" %}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,42 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n socialaccount %}
|
|
||||||
{% load allauth account %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="grid mt-5">
|
|
||||||
<a class="cell button is-primary py-5 is-size-5 has-text-dark"
|
|
||||||
href="{% url "authentication:link_login_form" %}">{% trans "Connexion par e-mail" %}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<h2 class="subtitle">{% trans "Connexion via un compte tiers" %}</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{% get_providers as providers %}
|
|
||||||
|
|
||||||
<div class="grid mt-5">
|
|
||||||
{% for provider in providers %}
|
|
||||||
{% if provider.id == "openid" %}
|
|
||||||
{% for brand in provider.get_brands %}
|
|
||||||
<a class="cell button is-{{ provider.app.settings.color }} is-light py-5 is-size-4"
|
|
||||||
title="{{ brand.name }}"
|
|
||||||
href="{% provider_login_url provider openid=brand.openid_url process="login" %}"><b>{{ brand.name }}</b></a>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a class="cell button is-{{ provider.app.settings.color }} is-light py-5 is-size-5"
|
|
||||||
href="{% provider_login_url provider process="login" scope=scope auth_params=auth_params %}">{{ provider.name }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODO: Write a text explaining how the different methods work -->
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
{% block extra_body %}
|
|
||||||
{{ block.super }}
|
|
||||||
{% if PASSKEY_LOGIN_ENABLED %}
|
|
||||||
{% include "mfa/webauthn/snippets/login_script.html" with button_id="passkey_login" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock extra_body %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load allauth i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2 class="subtitle">{% trans "Déconnexion" %}</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p class="notification is-warning is-light has-text-centered">
|
|
||||||
{% trans "Êtes vous certain·e de vouloir vous déconnecter ?" %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'account_logout' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ redirect_field }}
|
|
||||||
<button class="button is-fullwidth" type="submit">{% trans "Se déconnecter" %}</button>
|
|
||||||
</form>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2 class="subtitle">{% trans "Connexion impossible" %}</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="notification is-warning is-light px-5 py-5 has-text-centered is-size-5">
|
|
||||||
{% blocktrans %}Vos informations ne permettent pas de vous identifier auprès de la DGNum.<br>Si vous pensez qu'il s'agit une erreur, merci de nous contacter à l'adresse : <a href="mailto:contact@dgnum.eu">contact@dgnum.eu</a>{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,5 +1,4 @@
|
||||||
{% load allauth %}
|
{% load allauth %}
|
||||||
|
|
||||||
{% comment %} djlint:off {% endcomment %}
|
{% comment %} djlint:off {% endcomment %}
|
||||||
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
|
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
|
||||||
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
|
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
|
||||||
|
|
1
src/shared/templates/allauth/elements/provider.html
Normal file
1
src/shared/templates/allauth/elements/provider.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<a class="cell button is-primary is-light" title="{{ attrs.name }}" href="{{ attrs.href }}">{{ attrs.name }}</a>
|
6
src/shared/templates/allauth/elements/provider_list.html
Normal file
6
src/shared/templates/allauth/elements/provider_list.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% load allauth %}
|
||||||
|
|
||||||
|
<div class="grid mt-5">
|
||||||
|
{% slot default %}
|
||||||
|
{% endslot %}
|
||||||
|
</div>
|
|
@ -1,3 +1,5 @@
|
||||||
|
{% load django_browser_reload i18n sass_tags static %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
|
@ -13,17 +15,58 @@
|
||||||
<body>
|
<body>
|
||||||
{% include "_hero.html" %}
|
{% include "_hero.html" %}
|
||||||
|
|
||||||
<section class="section">
|
<section class="section container">
|
||||||
<div id="notifications">
|
<nav class="level">
|
||||||
{% for message in messages %}
|
{% if user.is_authenticated %}
|
||||||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
{% url 'account_email' as email_url %}
|
||||||
<button class="delete"></button>
|
{% if email_url %}
|
||||||
{{ message|safe }}
|
<li class="level-item button is-light">
|
||||||
</article>
|
<a href="{{ email_url }}">{% trans "Change Email" %}</a>
|
||||||
{% endfor %}
|
</li>
|
||||||
</div>
|
{% endif %}
|
||||||
|
{% url 'account_change_password' as change_password_url %}
|
||||||
|
{% if change_password_url %}
|
||||||
|
<li class="level-item button is-light">
|
||||||
|
<a href="{{ change_password_url }}">{% trans "Change Password" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% url 'mfa_index' as mfa_url %}
|
||||||
|
{% if mfa_url %}
|
||||||
|
<li class="level-item button is-light">
|
||||||
|
<a href="{{ mfa_url }}">{% trans "Two-Factor Authentication" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% url 'usersessions_list' as usersessions_list_url %}
|
||||||
|
{% if usersessions_list_url %}
|
||||||
|
<li class="level-item button is-light">
|
||||||
|
<a href="{{ usersessions_list_url }}">{% trans "Sessions" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% url 'account_logout' as logout_url %}
|
||||||
|
{% if logout_url %}
|
||||||
|
<li class="level-item button is-light">
|
||||||
|
<a href="{{ logout_url }}">{% trans "Sign Out" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% url 'account_login' as login_url %}
|
||||||
|
{% if login_url %}
|
||||||
|
<li class="level-item button is-light has-text-weight-bold">
|
||||||
|
<a href="{{ login_url }}">{% trans "Sign In" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% url 'account_signup' as signup_url %}
|
||||||
|
{% if signup_url %}
|
||||||
|
<li class="level-item button is-light has-text-weight-bold">
|
||||||
|
<a href="{{ signup_url }}">{% trans "Sign Up" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="content container">
|
<hr>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,26 +5,15 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="keywords" content="dgnum,dgsi,ens" />
|
<meta name="keywords" content="dgnum,dgsi,ens" />
|
||||||
<meta name="description" content="Système d'information de la DGNum" />
|
<meta name="description" content="Système d'information de la DGNum" />
|
||||||
<meta name="htmx-config"
|
|
||||||
content='{"defaultSwapStyle":"outerHTML","requestClass":"is-loading"}' />
|
|
||||||
<title>DGNum</title>
|
<title>DGNum</title>
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
{% endblock extra_head %}
|
|
||||||
|
|
||||||
{% include "_links.html" %}
|
{% include "_links.html" %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
<body>
|
||||||
{% include "_hero.html" %}
|
{% include "_hero.html" %}
|
||||||
|
|
||||||
<section class="container is-max-widescreen py-6 px-4">
|
<section class="section">
|
||||||
<div id="notifications">
|
|
||||||
{% for message in messages %}
|
|
||||||
{% include "partials/notification.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</section>
|
</section>
|
||||||
|
|
7
src/shared/templates/home.html
Normal file
7
src/shared/templates/home.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ul>
|
||||||
|
<li><a href="profiles.html">Profils WiFi</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
51
src/shared/templates/iosprofile.xml
Normal file
51
src/shared/templates/iosprofile.xml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>AutoJoin</key>
|
||||||
|
<true/>
|
||||||
|
<key>CaptiveBypass</key>
|
||||||
|
<false/>
|
||||||
|
<key>EncryptionType</key>
|
||||||
|
<string>WPA2</string>
|
||||||
|
<key>HIDDEN_NETWORK</key>
|
||||||
|
<false/>
|
||||||
|
<key>IsHotspot</key>
|
||||||
|
<false/>
|
||||||
|
<key>Password</key>
|
||||||
|
<string>{{ radius_credential }}</string>
|
||||||
|
<key>PayloadDescription</key>
|
||||||
|
<string>Configures Wi-Fi settings</string>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DGNum WiFi</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>com.apple.wifi.managed.5A2AE473-F6B7-4D60-9778-B25D26317C41</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.wifi.managed</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>5A2AE473-F6B7-4D60-9778-B25D26317C41</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>ProxyType</key>
|
||||||
|
<string>None</string>
|
||||||
|
<key>SSID_STR</key>
|
||||||
|
<string>DGNum</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>DGNum WiFi</string>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>WiFi-PSK-Sample.D5B78A3C-CDA8-471F-984C-06F977EF870C</string>
|
||||||
|
<key>PayloadRemovalDisallowed</key>
|
||||||
|
<false/>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>444C9683-221C-49AF-997D-2B6B84710DAA</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
29
src/shared/templates/login.html
Normal file
29
src/shared/templates/login.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n socialaccount %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="fixed-grid">
|
||||||
|
<div class="grid">
|
||||||
|
<a href="{% provider_login_url 'kanidm' %}"
|
||||||
|
class="cell has-background-primary-dark p-6 has-radius-normal has-text-centered has-text-white">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="ti ti-login"></i>
|
||||||
|
</span>
|
||||||
|
<span><b>{% trans "Connexion via la DGNum" %}</b></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% provider_login_url 'kanidm' %}"
|
||||||
|
class="cell has-background-primary p-6 has-radius-normal has-text-centered has-text-white">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="ti ti-login"></i>
|
||||||
|
</span>
|
||||||
|
<span><b>{% trans "Connexion via l'ENS" %}</b></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -1,17 +0,0 @@
|
||||||
Bonjour,
|
|
||||||
|
|
||||||
Une demande de réinitialisation de votre mot de passe DGNum a été effectuée.
|
|
||||||
|
|
||||||
Pour mettre à jour vos moyens de connexion, merci de vous rendre à l'adresse : {{ link }}
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
Hello,
|
|
||||||
|
|
||||||
A request to reset your DGNum password has been made.
|
|
||||||
|
|
||||||
To update your login details, please go to: {{ link }}
|
|
||||||
|
|
||||||
|
|
||||||
Bien cordialement,
|
|
||||||
La Délégation Générale Numérique
|
|
|
@ -1,21 +0,0 @@
|
||||||
Bonjour,
|
|
||||||
|
|
||||||
Une demande de connexion par e-mail vient d'être effectué pour votre compte.
|
|
||||||
|
|
||||||
Pour vous connecter à DG·SI, cliquez sur le lien suivant : {{ link }}
|
|
||||||
|
|
||||||
Ce-dernier est à usage unique et valable 15 minutes.
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
Hello,
|
|
||||||
|
|
||||||
An e-mail connection request has just been made for your account.
|
|
||||||
|
|
||||||
To connect to DG-SI, click on the following link: {{ link }}
|
|
||||||
|
|
||||||
It is single-use and valid for 15 minutes.
|
|
||||||
|
|
||||||
|
|
||||||
Bien cordialement,
|
|
||||||
La Délégation Générale Numérique
|
|
|
@ -1,4 +0,0 @@
|
||||||
<article class="notification is-light has-text-centered {{ message.tags }}">
|
|
||||||
<button class="delete"></button>
|
|
||||||
{{ message|safe }}
|
|
||||||
</article>
|
|
8
src/shared/templates/profiles.html
Normal file
8
src/shared/templates/profiles.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="buttons">
|
||||||
|
<a class="button is-link is-soft">iOS</a>
|
||||||
|
<a class="button is-danger is-soft">Android</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue