Quellcode durchsuchen

Funciona con passkeys

Tino vor 3 Monaten
Ursprung
Commit
197a92a95c

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+__pycache__
+*.sqlite3

+ 1 - 1
K8S/.current_version

@@ -1 +1 @@
-alpine-11
+alpine-12

+ 1 - 1
K8S/.version

@@ -1 +1 @@
-alpine-12
+alpine-13

+ 26 - 0
PASSKEYS.md

@@ -0,0 +1,26 @@
+Problemas encontrados y soluciones aplicadas
+1. FIDO_SERVER_ID fijo para producción nunca funcionaba en local
+Archivo: reymota/settings.py:174-178
+El RP ID de WebAuthn debe coincidir con el dominio del navegador. "nc.reymota.es" no funciona en localhost. Ahora es condicional:
+if LOCAL:
+    FIDO_SERVER_ID="localhost"
+else:
+    FIDO_SERVER_ID="nc.reymota.es"
+Además cambié FIDO_SERVER_NAME de "TestApp" a "ReyMota.es".
+2. passkeys.js incluido DOS VECES en el login
+Archivo: templates/registration/login.html
+Estaba dos veces: línea 66 (dentro del <form>, sin allow_password) y línea 92 (al final). Eliminé la del interior del formulario.
+3. CRÍTICO: LoginView de Django + AuthenticationForm no pueden procesar passkeys
+Archivos: reymotausers/auth.py, accounts/urls.py
+El problema de raíz: Django's AuthenticationForm requiere username + password para llamar a authenticate(). El login con passkey envía el formulario sin password, así que authenticate() nunca se ejecutaba y el backend PasskeyModelBackend jamás procesaba el passkey.
+Solución: Reemplacé el LoginView de Django por una vista personalizada que:
+- Si el campo passkeys tiene datos → llama a authenticate(request) directamente → el backend procesa la aserción WebAuthn
+- Si no → usa AuthenticationForm normal para login con email+password
+Flujo que ahora funciona:
+Botón "Login by Passkeys"
+  → JS llama a GET /passkeys/auth/begin/ (guarda fido2_state en sesión)
+  → navigator.credentials.get() → assertion
+  → submit del form con passkeys=<JSON>
+  → loginView detecta passkeys → authenticate(request)
+  → PasskeyModelBackend → auth_complete() → login()
+  → redirect a principal

+ 2 - 1
src/accounts/urls.py

@@ -1,8 +1,9 @@
 # accounts/urls.py
 # accounts/urls.py
 from django.urls import path
 from django.urls import path
 from django.contrib.auth import views as auth_views
 from django.contrib.auth import views as auth_views
+from reymotausers.auth import loginView
 
 
 urlpatterns = [
 urlpatterns = [
-    path('login/', auth_views.LoginView.as_view(), name='login'),
+    path('login/', loginView, name='login'),
     path('logout/', auth_views.LogoutView.as_view(), name='logout'),
     path('logout/', auth_views.LogoutView.as_view(), name='logout'),
 ]
 ]

+ 48 - 0
src/lyrics/migrations/0001_initial.py

@@ -0,0 +1,48 @@
+# Generated by Django 6.0.2 on 2026-05-13 13:07
+
+import django.core.validators
+import django.db.models.deletion
+import lyrics.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Artista',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('nombre', models.CharField(max_length=200)),
+                ('biografia', models.TextField(blank=True, null=True)),
+                ('foto', models.ImageField(blank=True, null=True, upload_to='artistas/')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Album',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=200)),
+                ('year', models.PositiveBigIntegerField(default=2026, validators=[django.core.validators.MinValueValidator(1945), lyrics.models.max_value_current_year])),
+                ('cover_image', models.ImageField(blank=True, null=True, upload_to='cover_image/')),
+                ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lyrics.artista')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Song',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=200)),
+                ('year', models.DecimalField(decimal_places=0, max_digits=4)),
+                ('lyrics', models.TextField()),
+                ('pista', models.DecimalField(blank=True, decimal_places=0, max_digits=5, null=True)),
+                ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='song', to='lyrics.album')),
+                ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lyrics.artista')),
+            ],
+        ),
+    ]

+ 0 - 0
src/lyrics/migrations/__init__.py


+ 1 - 0
src/requirements.txt

@@ -24,3 +24,4 @@ weasyprint==68.0
 webencodings==0.5.1
 webencodings==0.5.1
 zopfli==0.4.0
 zopfli==0.4.0
 django-passkeys>=2.1
 django-passkeys>=2.1
+djangorestframework-simplejwt>=5.0

+ 52 - 39
src/reymota/settings.py

@@ -34,7 +34,7 @@ SECRET_KEY = 'django-insecure-vu#zk4g8pj-qoov#8^i$&s8n_ipp2r3h+o$z1w(1%d=6+i@erm
 # SECURITY WARNING: don't run with debug turned on in production!
 # SECURITY WARNING: don't run with debug turned on in production!
 # DEBUG = True
 # DEBUG = True
 DEBUG = os.environ["DEBUG"] == 'True'
 DEBUG = os.environ["DEBUG"] == 'True'
-OPENSHIFT = os.environ["OPENSHIFT"] == 'True'
+LOCAL = os.environ["LOCAL"] == 'True'
 
 
 ALLOWED_HOSTS = [".reymota.es", ".reymota.lab",".vmcluster.lab"]
 ALLOWED_HOSTS = [".reymota.es", ".reymota.lab",".vmcluster.lab"]
 
 
@@ -97,16 +97,25 @@ WSGI_APPLICATION = 'reymota.wsgi.application'
 # Database
 # Database
 # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
 # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
 
 
-DATABASES = {
-    "default": {
-        "ENGINE": "django.db.backends.postgresql",
-        "NAME": "reymota",
-        "USER": "creylopez",
-        "PASSWORD": "Dsa-0213",
-        "HOST": os.environ.get('SQL_HOST','postgres-cluster-rw.reymota-pg-cluster.svc.cluster.local'),
-        "PORT": "5432",
+if LOCAL:
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.sqlite3',
+            'NAME': os.path.join(BASE_DIR, 'debug.sqlite3'),
+        }
+    }
+
+else:
+    DATABASES = {
+        "default": {
+            "ENGINE": "django.db.backends.postgresql",
+            "NAME": "reymota",
+            "USER": "creylopez",
+            "PASSWORD": "Dsa-0213",
+            "HOST": os.environ.get('SQL_HOST','postgres-cluster-rw.reymota-pg-cluster.svc.cluster.local'),
+            "PORT": "5432",
+        }
     }
     }
-}
 
 
 # Password validation
 # Password validation
 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -162,8 +171,11 @@ LOGOUT_REDIRECT_URL = 'principal'
 
 
 AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend']
 AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend']
 
 
+#if LOCAL:
+    #FIDO_SERVER_ID="localhost"
+#else:
 FIDO_SERVER_ID="nc.reymota.es"      # Server rp id for FIDO2, it the full domain of your project
 FIDO_SERVER_ID="nc.reymota.es"      # Server rp id for FIDO2, it the full domain of your project
-FIDO_SERVER_NAME="TestApp"
+FIDO_SERVER_NAME="ReyMota.es"
 KEY_ATTACHMENT = None  # Set None to allow all authenticator attachment
 KEY_ATTACHMENT = None  # Set None to allow all authenticator attachment
 
 
 AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
 AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
@@ -171,36 +183,37 @@ AUTH_USER_MODEL = "reymotausers.ReyMotaUser"
 MEDIA_ROOT = BASE_DIR / "mediafiles"
 MEDIA_ROOT = BASE_DIR / "mediafiles"
 MEDIA_URL = '/media/'
 MEDIA_URL = '/media/'
 
 
-LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'formatters': {
-        'custom_format': {
-            'format': '{levelname} {asctime} {module} {message}',
-            'style': '{',
-        },
-        'formato_personalizado': {
-            'format': '[{asctime}] [{levelname}] [{module}] <<< {message} >>>',
-            'style': '{',
+if LOCAL == False:
+    LOGGING = {
+        'version': 1,
+        'disable_existing_loggers': False,
+        'formatters': {
+            'custom_format': {
+                'format': '{levelname} {asctime} {module} {message}',
+                'style': '{',
+            },
+            'formato_personalizado': {
+                'format': '[{asctime}] [{levelname}] [{module}] <<< {message} >>>',
+                'style': '{',
+            },
         },
         },
-    },
-    'handlers': {
-        'console': {
-            'level': 'DEBUG',
-            'class': 'logging.StreamHandler',
-            'formatter': 'formato_personalizado',
+        'handlers': {
+            'console': {
+                'level': 'DEBUG',
+                'class': 'logging.StreamHandler',
+                'formatter': 'formato_personalizado',
+            },
         },
         },
-    },
-    'loggers': {
-        'django': {
-            'handlers': ['console'],
-            'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
+        'loggers': {
+            'django': {
+                'handlers': ['console'],
+                'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
+            },
+            'repostajes': {
+                'handlers': ['console'],
+                'level': 'INFO',
+            }
         },
         },
-        'repostajes': {
-            'handlers': ['console'],
-            'level': 'INFO',
-        }
-    },
-}
+    }
 
 
 ADMINS = [('Tino', 'creylopez@yahoo.es')]
 ADMINS = [('Tino', 'creylopez@yahoo.es')]

+ 1 - 2
src/reymota/urls.py

@@ -24,9 +24,8 @@ from . import views
 
 
 urlpatterns = [
 urlpatterns = [
     path('marcelo/', admin.site.urls),
     path('marcelo/', admin.site.urls),
-    path(r'^passkeys/', include('passkeys.urls')),
 
 
-    path('repostajes/', include('repostajes.urls')),
+    path('passkeys/', include('passkeys.urls')),
 
 
     path('lyrics/', include('lyrics.urls')),
     path('lyrics/', include('lyrics.urls')),
 
 

+ 77 - 0
src/reymotausers/auth.py

@@ -0,0 +1,77 @@
+import json
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.forms import AuthenticationForm
+from django.shortcuts import render, redirect
+from rest_framework.exceptions import AuthenticationFailed
+from rest_framework.permissions import AllowAny
+from rest_framework.response import Response
+from rest_framework.serializers import Serializer, CharField
+from rest_framework.views import APIView
+
+from passkeys.api.token_backends import get_token_response
+
+def loginView(request):
+    if request.method == "POST":
+        passkeys_raw = request.POST.get("passkeys", "")
+        if passkeys_raw:
+            user = authenticate(request)
+            if user:
+                login(request, user)
+                return redirect(request.POST.get("next", "") or "principal")
+            form = AuthenticationForm(request)
+            form.add_error(None, "Passkey authentication failed. Try again.")
+        else:
+            form = AuthenticationForm(request, data=request.POST)
+            if form.is_valid():
+                user = form.get_user()
+                login(request, user)
+                return redirect(request.POST.get("next", "") or "principal")
+    else:
+        form = AuthenticationForm(request)
+    return render(request, "registration/login.html", {"form": form})
+
+# def create_session(request,username):
+#     user=User.objects.get(username=username)
+#     user.backend='django.contrib.auth.backends.ModelBackend'
+#     login(request, user)
+#     return HttpResponseRedirect(reverse('home'))
+
+
+def logoutView(request):
+    logout(request) # pragma: no cover
+    return  render(request,"logout.html",{}) # pragma: no cover
+
+
+class LoginSerializer(Serializer):
+    username = CharField()
+    password = CharField()
+
+
+class LoginAPIView(APIView):
+    """
+    Username + password login API.
+
+    Returns the same token format as passkey authentication
+    (JWT, DRF Token, or session depending on project config).
+    Use this to get an initial token before registering passkeys.
+    """
+
+    permission_classes = [AllowAny]
+    serializer_class = LoginSerializer
+
+    def post(self, request):
+        serializer = LoginSerializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        user = authenticate(
+            request,
+            username=serializer.validated_data['username'],
+            password=serializer.validated_data['password'],
+        )
+        if user is None:
+            raise AuthenticationFailed("Invalid username or password")
+        token_data = get_token_response(user, request)
+        return Response({
+            'user_id': user.pk,
+            'username': user.get_username(),
+            **token_data,
+        })

+ 34 - 0
src/reymotausers/migrations/0001_initial.py

@@ -0,0 +1,34 @@
+# Generated by Django 6.0.2 on 2026-05-13 13:07
+
+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='ReyMotaUser',
+            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')),
+                ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
+                ('foto', models.ImageField(blank=True, default='profile_images/default.jpg', upload_to='profile_images')),
+                ('is_staff', models.BooleanField(default=False)),
+                ('is_active', models.BooleanField(default=True)),
+                ('nombre', models.CharField(blank=True, max_length=200, null=True)),
+                ('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={
+                'abstract': False,
+            },
+        ),
+    ]

+ 0 - 0
src/reymotausers/migrations/__init__.py


+ 8 - 2
src/reymotausers/urls.py

@@ -1,9 +1,15 @@
-from django.urls import path
-
+from django.urls import path,include
+from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
 from . import views
 from . import views
 from .views import api_lista_usuarios, api_detalle_usuario
 from .views import api_lista_usuarios, api_detalle_usuario
+from . import auth
 
 
 urlpatterns = [
 urlpatterns = [
     path('api/usuarios/', api_lista_usuarios, name='api_lista_usuarios'),
     path('api/usuarios/', api_lista_usuarios, name='api_lista_usuarios'),
     path('api/usuarios/<int:usuario_id>/', api_detalle_usuario, name='api_detalle_usuario'),
     path('api/usuarios/<int:usuario_id>/', api_detalle_usuario, name='api_detalle_usuario'),
+    path('api/passkeys/', include('passkeys.api.urls')),
+    path('api/auth/login', auth.LoginAPIView.as_view(), name='api_login'),
+    path('api/auth/token', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+    path('api/auth/token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
+    path('api/auth/token/verify', TokenVerifyView.as_view(), name='token_verify'),
 ]
 ]

+ 3 - 2
src/templates/_head.html

@@ -16,8 +16,9 @@
     
     
     <!-- FontAwesome JS-->
     <!-- FontAwesome JS-->
     <script defer src="{% static 'plugins/fontawesome/js/all.min.js' %}"></script>
     <script defer src="{% static 'plugins/fontawesome/js/all.min.js' %}"></script>
+
+    <!-- jQuery (required for passkeys management) -->
+    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
     
     
     <!-- App CSS -->  
     <!-- App CSS -->  
     <link id="theme-style" rel="stylesheet" href="{% static 'css/portal.css' %}">
     <link id="theme-style" rel="stylesheet" href="{% static 'css/portal.css' %}">
-
-</head> 

+ 0 - 5
src/templates/acerca_de.html

@@ -13,12 +13,7 @@
                         <li><strong>Nombre de la aplicación:</strong> {{ app_name }}</li>
                         <li><strong>Nombre de la aplicación:</strong> {{ app_name }}</li>
                         <li><strong>Versión:</strong> {{ app_version }}</li>
                         <li><strong>Versión:</strong> {{ app_version }}</li>
 
 
-                    {% if 'OPENSHIFT'|muestra_version == 'True' %}
-                        <li><strong>Versión de la imagen:</strong> openshift </li>
-                    {% else %}
                         <li><strong>Versión de la imagen:</strong> {{ img_version }}</li>
                         <li><strong>Versión de la imagen:</strong> {{ img_version }}</li>
-                    {% endif %}
-
                     </ul>
                     </ul>
                 </div><!--//app-card-body-->
                 </div><!--//app-card-body-->
             </div><!--//app-card-->
             </div><!--//app-card-->

+ 2 - 0
src/templates/base.html

@@ -1,6 +1,8 @@
 {% load static %}
 {% load static %}
 
 
 {% include "_head.html"  %}
 {% include "_head.html"  %}
+{% block head %}{% endblock %}
+</head>
 
 
 <body class="app">
 <body class="app">
   <header class="app-header fixed-top">	   	            
   <header class="app-header fixed-top">	   	            

+ 0 - 4
src/templates/index.html

@@ -12,11 +12,7 @@
         <div class="inner">
         <div class="inner">
             <div class="app-card-body p-3 p-lg-4">
             <div class="app-card-body p-3 p-lg-4">
 
 
-             {% if 'OPENSHIFT'|muestra_version == 'True' %}
-                <h3 class="mb-3">¡Bienvenido a sitio REYMOTA.ES en Openshift!</h3>
-             {% else %}
                 <h3 class="mb-3">¡Bienvenido a sitio REYMOTA.ES en K8S!</h3>
                 <h3 class="mb-3">¡Bienvenido a sitio REYMOTA.ES en K8S!</h3>
-             {% endif %}
 
 
                 <div class="row gx-5 gy-3">
                 <div class="row gx-5 gy-3">
                     <!--
                     <!--

+ 159 - 0
src/templates/passkeys/manage.html

@@ -0,0 +1,159 @@
+{% extends "passkeys/base.html" %}
+{% load static %}
+{% block head %}
+{{block.super}}
+        <script type="application/javascript" src="{% static 'passkeys/js/base64url.js'%}"></script>
+    <script type="application/javascript" src="{% static 'passkeys/js/helpers.js'%}"></script>
+    <script type="application/javascript">
+    var MakeCredReq = (makeCredReq) => {
+            makeCredReq.publicKey.challenge = base64url.decode(makeCredReq.publicKey.challenge);
+            makeCredReq.publicKey.user.id   = base64url.decode(makeCredReq.publicKey.user.id);
+
+            for(let excludeCred of makeCredReq.publicKey.excludeCredentials) {
+                excludeCred.id = base64url.decode(excludeCred.id);
+            }
+
+            return makeCredReq
+        }
+    function begin_reg(){
+    fetch('{% url 'passkeys:reg_begin' %}',{}).then(function(response) {
+      if(response.ok)
+      {
+          return response.json().then(function (req){
+              return MakeCredReq(req)
+          });
+      }
+      throw new Error('Error getting registration data!');
+    }).then(function(options) {
+        if (!navigator.credentials) {
+            throw new Error('WebAuthn is not available. Ensure the page is served over HTTPS (secure context).');
+        }
+        return navigator.credentials.create(options);
+    }).then(function(attestation) {
+        attestation["key_name"] = $("#key_name").val();
+      return fetch('{% url 'passkeys:reg_complete' %}', {
+          headers:{'X-CSRFToken': "{{csrf_token}}"},
+        method: 'POST',
+        body: JSON.stringify(publicKeyCredentialToJSON(attestation))
+      });
+    }).then(function(response) {
+
+        var stat = response.ok ? 'successful' : 'unsuccessful';
+        return response.json()
+    }).then(function (res)
+        {
+      if (res["status"] =='OK')
+        $("#res").html("<div class='alert alert-success'>Registered Successfully, <a href='{% url "passkeys:home"%}'> Refresh</a></div>")
+      else
+        $("#res").html("<div class='alert alert-danger'>Registration Failed as " + res["message"] + ", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> </div>")
+
+
+    }, function(reason) {
+       $("#res").html("<div class='alert alert-danger'>Registration Failed as " +reason +", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> </div>")
+    })
+    }
+
+
+
+
+    function confirmDel(id) {
+        $.ajax({
+            url:"{% url 'passkeys:delKey' %}",
+            method:"POST",
+            data:{"id":id,"csrfmiddlewaretoken":"{{ csrf_token }}"},
+            success:function (data) {
+                alert(data)
+                window.location= "{%url 'passkeys:home'%}";
+            }
+        })
+    }
+
+    function start(){
+        if (!navigator.credentials) {
+            alert("WebAuthn is not available. This feature requires a secure context (HTTPS).");
+            return;
+        }
+        $("#modal-title").html("Enter a token name")
+        $("#modal-body").html(`<p>Please enter a name for your new token</p>
+                                <input type="text" placeholder="e.g Laptop, PC" id="key_name" class="form-control"/><br/>
+                                <div id="res"></div>
+                                `)
+        $("#actionBtn").remove();
+        $("#modal-footer").prepend(`<button id='actionBtn' class='btn btn-success' onclick="begin_reg()">Start</button>`)
+        $("#popUpModal").modal('show')
+    }
+    function deleteKey(id,name)
+    {
+        $("#modal-title").html("Confirm Delete")
+        $("#modal-body").html(`Are you sure you want to delete '${name}'? you may lose access to your system if this your only 2FA.`);
+        $("#actionBtn").remove()
+        $("#modal-footer").prepend("<button id='actionBtn' class='btn btn-danger' onclick='confirmDel("+id+")'>Confirm Deletion</button>")
+        $("#popUpModal").modal('show')
+    }
+
+    function toggleKey(id) {
+        $.ajax({
+            url:"{% url 'passkeys:toggle' %}",
+            data:{"id":id,"csrfmiddlewaretoken":"{{ csrf_token }}"},
+            method:"POST",
+            success:function (data) {
+                if (data == "Error")
+                    $("#toggle_"+id).toggle()
+
+            },
+            error:function (data) {
+                $("#toggle_"+id).toggle()
+            }
+        })
+    }
+    {%if enroll%}
+        $(document).ready(start)
+    {%endif%}
+
+    </script>
+    <link href="{% static  'passkeys/css/bootstrap-toggle.min.css' %}" rel="stylesheet">
+    <script src="{% static 'passkeys/js/bootstrap-toggle.min.js'%}"></script>
+{% endblock %}
+{% block content %}
+{{block.super}}
+    <br/>
+    <br/>
+    <div class="container">
+        <div class="row">
+            <div class="offset-5 col-2" style="text-align: center">
+                <div class="btn-group">
+                    <button class="btn btn-success" onclick='start()'> Add Key</button>
+                </div>
+            </div>
+        </div>
+        <br/>
+       <table class="table table-striped">
+           <tr>
+               <th>Name</th>
+               <th>Date Added</th>
+               <th>Platform</th>
+               <th>Last Used</th>
+               <th>Status</th>
+               <th>Delete</th>
+           </tr>
+            {% if keys %}
+                {% for key in keys %}
+                <tr>
+
+                    <td>{{ key.name }}</td>
+                    <td>{{ key.added_on }}</td>
+                    <td>{{ key.platform }}</td>
+                    <td>{% if key.last_used %}{{ key.last_used }}{% else %}Never{% endif %}</td>
+                    <td><input type="checkbox" id="toggle_{{ key.id }}" {% if key.enabled %}checked{% endif %} data-onstyle="success" data-offstyle="danger"  onchange="toggleKey({{ key.id }})" data-toggle="toggle" class="status_chk"></td>
+                    <td><a href="javascript:void(0)" onclick="deleteKey({{ key.id }},'{{ key.name }}')"> <span class="fa fa-trash fa-solid fa-trash-can bi bi-trash-fill"></span></a></td>
+                </tr>
+                {% endfor %}
+            {% else %}
+            <tr>
+                <td colspan="7" align="center">You didn't have any keys yet.</td>
+            </tr>
+        {% endif %}
+        </table>
+    </div>
+    {% include "passkeys/modal.html" %}
+{% endblock %}

+ 3 - 4
src/templates/registration/login.html

@@ -34,7 +34,7 @@
                         </p>
                         </p>
                         {% endif %}
                         {% endif %}
 
 
-                        <form class="auth-form login-form" action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
+                        <form class="auth-form login-form" action="{{ request.path }}" method="post" id="login-form">{% csrf_token %}
                             <div class="text mb-3">
                             <div class="text mb-3">
                                 {{ form.username.errors }}
                                 {{ form.username.errors }}
                                 {{ form.username.label_tag }}
                                 {{ form.username.label_tag }}
@@ -48,7 +48,7 @@
                             </div>
                             </div>
                             <div class="password mb-3">
                             <div class="password mb-3">
                                 {{ form.password }}
                                 {{ form.password }}
-                                <input type="hidden" name="next" value="{{ next }}">
+                                <input type="hidden" name="next" value="{{ request.GET.next }}">
                             </div>
                             </div>
                             {% url 'admin_password_reset' as password_reset_url %}
                             {% url 'admin_password_reset' as password_reset_url %}
                             {% if password_reset_url %}
                             {% if password_reset_url %}
@@ -63,7 +63,6 @@
                             <div class="text-center">
                             <div class="text-center">
 			     <input type="hidden" name="passkeys" id="passkeys"/>
 			     <input type="hidden" name="passkeys" id="passkeys"/>
  <button class="btn btn-block btn-dark" type="button" onclick="authn('login-form')"><img src="{% static 'passkeys/imgs/FIDO-Passkey_Icon-White.png' %}" style="width: 24px"/> Login by Passkeys </button>
  <button class="btn btn-block btn-dark" type="button" onclick="authn('login-form')"><img src="{% static 'passkeys/imgs/FIDO-Passkey_Icon-White.png' %}" style="width: 24px"/> Login by Passkeys </button>
- {%include 'passkeys/passkeys.js' %}
                             </div>
                             </div>
                         </form>
                         </form>
                     </div>
                     </div>
@@ -89,6 +88,6 @@
         </div><!--//auth-background-col-->
         </div><!--//auth-background-col-->
 
 
     </div><!--//row-->
     </div><!--//row-->
-
+  {%include 'passkeys/passkeys.js' with allow_password=True %}
 </body>
 </body>
 </html> 
 </html>