').addClass(this.$element.prop("checked")?this._onstyle:this._offstyle+" off").addClass(b).addClass(this.options.style);this.$element.wrap(g),a.extend(this,{$toggle:this.$element.parent(),$toggleOn:c,$toggleOff:d,$toggleGroup:f}),this.$toggle.append(f);var h=this.options.width||Math.max(c.outerWidth(),d.outerWidth())+e.outerWidth()/2,i=this.options.height||Math.max(c.outerHeight(),d.outerHeight());c.addClass("toggle-on"),d.addClass("toggle-off"),this.$toggle.css({width:h,height:i}),this.options.height&&(c.css("line-height",c.height()+"px"),d.css("line-height",d.height()+"px")),this.update(!0),this.trigger(!0)},c.prototype.toggle=function(){this.$element.prop("checked")?this.off():this.on()},c.prototype.on=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._offstyle+" off").addClass(this._onstyle),this.$element.prop("checked",!0),void(a||this.trigger()))},c.prototype.off=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._onstyle).addClass(this._offstyle+" off"),this.$element.prop("checked",!1),void(a||this.trigger()))},c.prototype.enable=function(){this.$toggle.removeAttr("disabled"),this.$element.prop("disabled",!1)},c.prototype.disable=function(){this.$toggle.attr("disabled","disabled"),this.$element.prop("disabled",!0)},c.prototype.update=function(a){this.$element.prop("disabled")?this.disable():this.enable(),this.$element.prop("checked")?this.on(a):this.off(a)},c.prototype.trigger=function(b){this.$element.off("change.bs.toggle"),b||this.$element.change(),this.$element.on("change.bs.toggle",a.proxy(function(){this.update()},this))},c.prototype.destroy=function(){this.$element.off("change.bs.toggle"),this.$toggleGroup.remove(),this.$element.removeData("bs.toggle"),this.$element.unwrap()};var d=a.fn.bootstrapToggle;a.fn.bootstrapToggle=b,a.fn.bootstrapToggle.Constructor=c,a.fn.toggle.noConflict=function(){return a.fn.bootstrapToggle=d,this},a(function(){a("input[type=checkbox][data-toggle^=toggle]").bootstrapToggle()}),a(document).on("click.bs.toggle","div[data-toggle^=toggle]",function(b){var c=a(this).find("input[type=checkbox]");c.bootstrapToggle("toggle"),b.preventDefault()})}(jQuery);
-//# sourceMappingURL=bootstrap-toggle.min.js.map
\ No newline at end of file
+/* Copyright Notice
+ * bootstrap5-toggle v5.0.4
+ * https://palcarazm.github.io/bootstrap5-toggle/
+ * @author 2011-2014 Min Hur (https://github.com/minhur)
+ * @author 2018-2019 Brent Ely (https://github.com/gitbrent)
+ * @author 2022 Pablo Alcaraz Martínez (https://github.com/palcarazm)
+ * @funding GitHub Sponsors
+ * @see https://github.com/sponsors/palcarazm
+ * @license MIT
+ * @see https://github.com/palcarazm/bootstrap5-toggle/blob/master/LICENSE
+ */
+
+"use strict";!function(){class s{constructor(e,t){const i="BOOTSTRAP TOGGLE DEPRECATION CHECK -- a0Jhux0QySypjjs4tLtEo8xT2kx0AbYaq9K6mgNjWSs0HF0L8T8J0M0o3Kr7zkm7 --",s="attribute",n="option",l=function(e,t,i){console.warn(`Bootstrap Toggle deprecation warning: Using ${t} ${e} is deprected. Use ${i} instead.`)},o="On",a="primary",h=null,d=null,r="Off",m="secondary",g=null,b=null,c="",u="",f=null,p=null,v=0,A=!1,y=null;t=t||{},this.element=e,this.options={onlabel:this.element.getAttribute("data-onlabel")||t.onlabel||i||o,onstyle:this.element.getAttribute("data-onstyle")||t.onstyle||a,onvalue:this.element.getAttribute("value")||this.element.getAttribute("data-onvalue")||t.onvalue||h,ontitle:this.element.getAttribute("data-ontitle")||t.ontitle||this.element.getAttribute("title")||d,offlabel:this.element.getAttribute("data-offlabel")||t.offlabel||i||r,offstyle:this.element.getAttribute("data-offstyle")||t.offstyle||m,offvalue:this.element.getAttribute("data-offvalue")||t.offvalue||g,offtitle:this.element.getAttribute("data-offtitle")||t.offtitle||this.element.getAttribute("title")||b,size:this.element.getAttribute("data-size")||t.size||c,style:this.element.getAttribute("data-style")||t.style||u,width:this.element.getAttribute("data-width")||t.width||f,height:this.element.getAttribute("data-height")||t.height||p,tabindex:this.element.getAttribute("tabindex")||t.tabindex||v,tristate:this.element.hasAttribute("tristate")||t.tristate||A,name:this.element.getAttribute("name")||t.name||y},this.options.onlabel===i&&(this.element.getAttribute("data-on")?(l(s,"data-on","data-onlabel"),this.options.onlabel=this.element.getAttribute("data-on")):t.on?(l(n,"on","onlabel"),this.options.onlabel=t.on):this.options.onlabel=o),this.options.offlabel===i&&(this.element.getAttribute("data-off")?(l(s,"data-off","data-offlabel"),this.options.offlabel=this.element.getAttribute("data-off")):t.off?(l(n,"off","offlabel"),this.options.offlabel=t.off):this.options.offlabel=r),this.render()}render(){function e(e){var t=window.getComputedStyle(e),e=e.offsetHeight,i=parseFloat(t.borderTopWidth);return e-parseFloat(t.borderBottomWidth)-i-parseFloat(t.paddingTop)-parseFloat(t.paddingBottom)}let t;switch(this.options.size){case"large":case"lg":t="btn-lg";break;case"small":case"sm":t="btn-sm";break;case"mini":case"xs":t="btn-xs";break;default:t=""}var i=document.createElement("span"),s=(i.setAttribute("class","btn btn-"+this.options.onstyle+" "+t),i.innerHTML=this.options.onlabel,this.options.ontitle&&i.setAttribute("title",this.options.ontitle),document.createElement("span")),n=(s.setAttribute("class","btn btn-"+this.options.offstyle+" "+t),s.innerHTML=this.options.offlabel,this.options.offtitle&&s.setAttribute("title",this.options.offtitle),document.createElement("span")),l=(n.setAttribute("class","toggle-handle btn "+t),document.createElement("div"));l.setAttribute("class","toggle-group"),l.appendChild(i),l.appendChild(s),l.appendChild(n);let o=document.createElement("div"),a=(o.setAttribute("class","toggle btn"),o.classList.add(this.element.checked?"btn-"+this.options.onstyle:"btn-"+this.options.offstyle),o.setAttribute("tabindex",this.options.tabindex),this.element.checked||o.classList.add("off"),this.options.size&&o.classList.add(t),this.options.style&&this.options.style.split(" ").forEach(e=>{o.classList.add(e)}),(this.element.disabled||this.element.readOnly)&&(o.classList.add("disabled"),o.setAttribute("disabled","disabled")),this.options.onvalue&&this.element.setAttribute("value",this.options.onvalue),null);this.options.offvalue&&((a=this.element.cloneNode()).setAttribute("value",this.options.offvalue),a.setAttribute("data-toggle","invert-toggle"),a.removeAttribute("id"),a.checked=!this.element.checked),this.element.parentElement.insertBefore(o,this.element),o.appendChild(this.element),a&&o.appendChild(a),o.appendChild(l),o.style.width=(this.options.width||Math.max(i.getBoundingClientRect().width,s.getBoundingClientRect().width)+n.getBoundingClientRect().width/2)+"px",o.style.height=(this.options.height||Math.max(i.getBoundingClientRect().height,s.getBoundingClientRect().height))+"px",i.classList.add("toggle-on"),s.classList.add("toggle-off"),this.options.height&&(i.style.lineHeight=e(i)+"px",s.style.lineHeight=e(s)+"px"),o.addEventListener("touchstart",e=>{this.#toggleActionPerformed(e)}),o.addEventListener("click",e=>{this.#toggleActionPerformed(e)}),o.addEventListener("keypress",e=>{" "==e.key&&this.#toggleActionPerformed(e)}),this.element.id&&document.querySelectorAll('label[for="'+this.element.id+'"]').forEach(e=>{e.addEventListener("touchstart",e=>{this.toggle(),o.focus()}),e.addEventListener("click",e=>{this.toggle(),o.focus()})}),this.ecmasToggle=o,this.invElement=a,this.element.bsToggle=this}#toggleActionPerformed(e){this.options.tristate?this.ecmasToggle.classList.contains("indeterminate")?(this.determinate(!0),this.toggle()):this.indeterminate():this.toggle(),e.preventDefault()}toggle(e=!1){this.element.checked?this.off(e):this.on(e)}on(e=!1){if(this.element.disabled||this.element.readOnly)return!1;this.ecmasToggle.classList.remove("btn-"+this.options.offstyle),this.ecmasToggle.classList.add("btn-"+this.options.onstyle),this.ecmasToggle.classList.remove("off"),this.element.checked=!0,this.invElement&&(this.invElement.checked=!1),e||this.trigger()}off(e=!1){if(this.element.disabled||this.element.readOnly)return!1;this.ecmasToggle.classList.remove("btn-"+this.options.onstyle),this.ecmasToggle.classList.add("btn-"+this.options.offstyle),this.ecmasToggle.classList.add("off"),this.element.checked=!1,this.invElement&&(this.invElement.checked=!0),e||this.trigger()}indeterminate(e=!1){if(!this.options.tristate||this.element.disabled||this.element.readOnly)return!1;this.ecmasToggle.classList.add("indeterminate"),this.element.indeterminate=!0,this.element.removeAttribute("name"),this.invElement&&(this.invElement.indeterminate=!0),this.invElement&&this.invElement.removeAttribute("name"),e||this.trigger()}determinate(e=!1){if(!this.options.tristate||this.element.disabled||this.element.readOnly)return!1;this.ecmasToggle.classList.remove("indeterminate"),this.element.indeterminate=!1,this.options.name&&this.element.setAttribute("name",this.options.name),this.invElement&&(this.invElement.indeterminate=!1),this.invElement&&this.options.name&&this.invElement.setAttribute("name",this.options.name),e||this.trigger()}enable(){this.ecmasToggle.classList.remove("disabled"),this.ecmasToggle.removeAttribute("disabled"),this.element.removeAttribute("disabled"),this.element.removeAttribute("readonly"),this.invElement&&(this.invElement.removeAttribute("disabled"),this.invElement.removeAttribute("readonly"))}disable(){this.ecmasToggle.classList.add("disabled"),this.ecmasToggle.setAttribute("disabled",""),this.element.setAttribute("disabled",""),this.element.removeAttribute("readonly"),this.invElement&&(this.invElement.setAttribute("disabled",""),this.invElement.removeAttribute("readonly"))}readonly(){this.ecmasToggle.classList.add("disabled"),this.ecmasToggle.setAttribute("disabled",""),this.element.removeAttribute("disabled"),this.element.setAttribute("readonly",""),this.invElement&&(this.invElement.removeAttribute("disabled"),this.invElement.setAttribute("readonly",""))}update(e){this.element.disabled?this.disable():this.element.readOnly?this.readonly():this.enable(),this.element.checked?this.on(e):this.off(e)}trigger(e){e||this.element.dispatchEvent(new Event("change",{bubbles:!0}))}destroy(){this.ecmasToggle.parentNode.insertBefore(this.element,this.ecmasToggle),this.ecmasToggle.parentNode.removeChild(this.ecmasToggle),delete this.element.bsToggle,delete this.ecmasToggle}}Element.prototype.bootstrapToggle=function(e,t){var i=this.bsToggle||new s(this,e);e&&"string"==typeof e&&("toggle"==e.toLowerCase()?i.toggle(t):"on"==e.toLowerCase()?i.on(t):"off"==e.toLowerCase()?i.off(t):"indeterminate"==e.toLowerCase()?i.indeterminate(t):"determinate"==e.toLowerCase()?i.determinate(t):"enable"==e.toLowerCase()?i.enable():"disable"==e.toLowerCase()?i.disable():"readonly"==e.toLowerCase()?i.readonly():"destroy"==e.toLowerCase()&&i.destroy())},"undefined"!=typeof window&&(window.onload=function(){document.querySelectorAll('input[type=checkbox][data-toggle="toggle"]').forEach(function(e){e.bootstrapToggle()})}),"undefined"!=typeof module&&module.exports&&(module.exports=s)}();
\ No newline at end of file
diff --git a/passkeys/static/passkeys/js/helpers.js b/passkeys/static/passkeys/js/helpers.js
deleted file mode 100644
index d6b70ab..0000000
--- a/passkeys/static/passkeys/js/helpers.js
+++ /dev/null
@@ -1,25 +0,0 @@
-var publicKeyCredentialToJSON = (pubKeyCred) => {
- if(pubKeyCred instanceof Array) {
- let arr = [];
- for(let i of pubKeyCred)
- arr.push(publicKeyCredentialToJSON(i));
-
- return arr
- }
-
- if(pubKeyCred instanceof ArrayBuffer) {
- return base64url.encode(pubKeyCred)
- }
-
- if(pubKeyCred instanceof Object) {
- let obj = {};
-
- for (let key in pubKeyCred) {
- obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
- }
-
- return obj
- }
-
- return pubKeyCred
- }
\ No newline at end of file
diff --git a/passkeys/static/passkeys/js/passkeys.js b/passkeys/static/passkeys/js/passkeys.js
new file mode 100644
index 0000000..e007c4b
--- /dev/null
+++ b/passkeys/static/passkeys/js/passkeys.js
@@ -0,0 +1,421 @@
+(function () {
+ 'use strict';
+
+ let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
+
+ // Use a lookup table to find the index.
+ let lookup = new Uint8Array(256);
+ for (let i = 0; i < chars.length; i++) {
+ lookup[chars.charCodeAt(i)] = i;
+ }
+
+ let encode = function (arraybuffer) {
+ let bytes = new Uint8Array(arraybuffer),
+ i, len = bytes.length, base64url = '';
+
+ for (i = 0; i < len; i += 3) {
+ base64url += chars[bytes[i] >> 2];
+ base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
+ base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
+ base64url += chars[bytes[i + 2] & 63];
+ }
+
+ if ((len % 3) === 2) {
+ base64url = base64url.substring(0, base64url.length - 1);
+ } else if (len % 3 === 1) {
+ base64url = base64url.substring(0, base64url.length - 2);
+ }
+
+ return base64url;
+ };
+
+ let decode = function (base64string) {
+ let bufferLength = base64string.length * 0.75,
+ len = base64string.length, i, p = 0,
+ encoded1, encoded2, encoded3, encoded4;
+
+ let bytes = new Uint8Array(bufferLength);
+
+ for (i = 0; i < len; i += 4) {
+ encoded1 = lookup[base64string.charCodeAt(i)];
+ encoded2 = lookup[base64string.charCodeAt(i + 1)];
+ encoded3 = lookup[base64string.charCodeAt(i + 2)];
+ encoded4 = lookup[base64string.charCodeAt(i + 3)];
+
+ bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
+ bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
+ bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
+ }
+
+ return bytes.buffer
+ };
+
+ let methods = {
+ 'decode': decode,
+ 'encode': encode
+ }
+
+ /**
+ * Exporting and stuff
+ */
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ module.exports = methods;
+
+ } else {
+ if (typeof define === 'function' && define.amd) {
+ define([], function () {
+ return methods
+ });
+ } else {
+ window.base64url = methods;
+ }
+ }
+})();
+
+
+(function () {
+ let urlBase = '/passkeys/';
+
+ if (location.protocol !== 'https:') {
+ console.error("Passkeys must work under secure context.");
+ }
+
+ const modalElem = document.querySelector('#django-passkeys-modal');
+ if (modalElem !== null) {
+ modalElem.addEventListener('hidden.bs.modal', event => {
+ window.location.href = urlBase;
+ });
+ }
+
+
+ window.conditionalUI = false;
+ window.conditionUIAbortController = new AbortController();
+ window.conditionUIAbortSignal = conditionUIAbortController.signal;
+
+
+ const urlAuthBegin = () => `${urlBase}auth/begin/`;
+ const urlKeyToggle = () => `${urlBase}toggle/`;
+ const urlKeyDelete = () => `${urlBase}del/`;
+ const urlRegBegin = () => `${urlBase}reg/begin/`;
+ const urlRegComplete = () => `${urlBase}reg/complete/`;
+
+
+ /**
+ * change the base url for the passkeys app
+ *
+ * @param url
+ */
+ const init = function (url) {
+ baseUrl = "";
+ if (!baseUrl.endsWith('/')) {
+ baseUrl += '/;'
+ }
+ }
+
+ const publicKeyCredentialToJSON = function (pubKeyCred) {
+ if (pubKeyCred instanceof Array) {
+ let arr = [];
+ for (let i of pubKeyCred)
+ arr.push(publicKeyCredentialToJSON(i));
+
+ return arr;
+ }
+
+ if (pubKeyCred instanceof ArrayBuffer) {
+ return base64url.encode(pubKeyCred);
+ }
+
+ if (pubKeyCred instanceof Object) {
+ let obj = {};
+
+ for (let key in pubKeyCred) {
+ obj[key] = publicKeyCredentialToJSON(pubKeyCred[key]);
+ }
+
+ return obj;
+ }
+
+ return pubKeyCred;
+ }
+
+
+ const checkConditionalUI = function (form) {
+ if (window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable) {
+ // Check if conditional mediation is available.
+ PublicKeyCredential.isConditionalMediationAvailable().then((result) => {
+ window.conditionalUI = result;
+ if (window.conditionalUI) {
+ authn(form, true);
+ }
+ });
+ }
+ }
+
+ const getAssertReq = function (getAssert) {
+ getAssert.publicKey.challenge = base64url.decode(getAssert.publicKey.challenge);
+
+ for (let allowCred of getAssert.publicKey.allowCredentials) {
+ allowCred.id = base64url.decode(allowCred.id);
+ }
+
+ return getAssert;
+ }
+
+ /**
+ * start the authentication process
+ *
+ * @param loginFormId
+ * @param conditionalUI
+ */
+ const authn = function (loginFormId, conditionalUI = false) {
+ fetch(urlAuthBegin(), {
+ method: 'GET',
+ }).then(function (response) {
+ if (response.ok) {
+ return response.json().then(function (req) {
+ return getAssertReq(req)
+ });
+ }
+ throw new Error('No credential available to authenticate!');
+ }).then(function (options) {
+ if (conditionalUI) {
+ options.mediation = 'conditional';
+ options.signal = window.conditionUIAbortSignal;
+ } else {
+ window.conditionUIAbortController.abort();
+ }
+
+ return navigator.credentials.get(options);
+ }).then(function (assertion) {
+ const passkeysInput = document.querySelector("#id_passkeys");
+ if (passkeysInput === null) {
+ console.error("Did you add the 'passkeys' hidden input field")
+ return
+ }
+ passkeysInput.value = JSON.stringify(publicKeyCredentialToJSON(assertion));
+
+ const loginForm = document.getElementById(loginFormId);
+
+ if (loginForm === null) {
+ console.error("Did you pass the correct form id to auth function")
+ return;
+ }
+
+ loginForm.submit()
+ });
+ }
+
+
+ /**
+ * Check if the platform supports passkeys.
+ *
+ * @param success_func
+ * @param fail_func
+ */
+ const checkPasskeysSupport = function (success_func, fail_func) {
+ PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
+ .then((available) => {
+ if (available) {
+ success_func();
+ } else {
+ fail_func()
+ }
+ }).catch((err) => {
+ // Something went wrong
+ console.error(err);
+ });
+ }
+
+ /**
+ * (De-)activate a passkey.
+ *
+ * @param id
+ */
+ const keyToggle = function (id) {
+ const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+
+ fetch(urlKeyToggle(), {
+ method: "post",
+ headers: {
+ "X-CSRFToken": csrfToken,
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: `id=${id}`
+ }
+ ).then(response => response.text()).then((response) => {
+ if (response === "Error")
+ document.querySelector("#toggle_" + id).toggle();
+ }).catch(() => {
+ document.querySelector("#toggle_" + id).toggle();
+ })
+ }
+
+ /**
+ * Confirm deleting a passkey.
+ *
+ * @param id
+ * @param name
+ */
+ const keyDeleteConfirm = function (id, name) {
+ const title = "Delete passkey";
+ const body = `
+
Are you sure you want to delete '${name}'?
+
You may lose access to this system if this your only 2FA.
+ `;
+
+ const button = document.createElement('button');
+ button.setAttribute('class', 'btn btn-danger btn-action');
+ button.onclick = () => keyDelete(id);
+ button.innerText = "Confirm";
+
+ showModal(title, body, button);
+ }
+
+ /**
+ * Delete a passkey.
+ *
+ * @param id
+ */
+ const keyDelete = function (id) {
+ const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+ fetch(urlKeyDelete(), {
+ method: "post",
+ headers: {
+ "X-CSRFToken": csrfToken,
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: `id=${id}`
+ })
+ .then(response => response.text())
+ .then(data => {
+ const title = "Confirm delete";
+ const body = '
The key has been deleted successfully.
'
+ updateModal(title, body);
+ });
+ }
+
+
+ const makeCredReq = function (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
+ }
+
+ /**
+ * Start passkey registration
+ */
+ const beginReg = function () {
+ fetch(urlRegBegin(), {}).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) {
+ //options.publicKey.attestation="direct"
+ return navigator.credentials.create(options);
+ }).then(function (attestation) {
+ attestation["key_name"] = document.getElementById("key_name").value;
+ return fetch(urlRegComplete(), {
+ method: 'POST',
+ body: JSON.stringify(publicKeyCredentialToJSON(attestation))
+ }
+ );
+ }).then(function (response) {
+ const stat = response.ok ? 'successful' : 'unsuccessful';
+ return response.json()
+ }).then(function (res) {
+ if (res["status"] === 'OK') {
+ const title = "Register new passkey";
+ const body = '
Passkey was successfully registered.
';
+ updateModal(title, body);
+ } else {
+ const title = "Register new passkey";
+ const body = `
Passkey registration failed!
${res["message"]}
`;
+ updateModal(title, body);
+ }
+ }, function (reason) {
+ const title = "Register new passkey";
+ const body = `
Passkey registration failed!
${reason}
`;
+ updateModal(title, body);
+ });
+ }
+
+ /**
+ * Open passkey registration dialog.
+ */
+ const registration = function () {
+ const title = "Register new passkey"
+ const body = `
+
Please enter a name for your new token.
+
+ `;
+
+ const button = document.createElement('button');
+ button.setAttribute('class', 'btn btn-primary btn-action');
+ button.onclick = beginReg;
+ button.innerText = "Start";
+
+ showModal(title, body, button);
+ }
+
+ /**
+ * Update and show the passkey modal.
+ *
+ * @param title
+ * @param body
+ * @param actionButton
+ */
+ const showModal = function (title, body, actionButton) {
+ updateModal(title, body, actionButton);
+ const modal = new bootstrap.Modal('#django-passkeys-modal', {});
+ modal.show();
+ }
+
+ /**
+ * Update the already visible passkey modal.
+ *
+ * @param title
+ * @param body
+ * @param actionButton
+ */
+ const updateModal = function (title, body, actionButton) {
+ // update title
+ const titleElem = document.querySelector('#django-passkeys-modal .modal-title');
+ titleElem.innerText = title;
+
+ // update body
+ const bodyElem = document.querySelector('#django-passkeys-modal .modal-body');
+ bodyElem.innerHTML = body;
+
+ // remove existing action buttons
+ for (let button of document.querySelectorAll('#django-passkeys-modal .btn-action')) {
+ button.remove();
+ }
+
+ // insert action button
+ if (actionButton) {
+ const footer = document.querySelector('#django-passkeys-modal .modal-footer');
+ footer.prepend(actionButton);
+ }
+ }
+
+ // create the global DjangoPasskey variable
+ if (typeof window.DjangoPasskeys === 'undefined') {
+ window.DjangoPasskeys = {
+ 'init': init,
+ 'checkConditionalUI': checkConditionalUI,
+ 'authn': authn,
+ 'checkPasskeysSupport': checkPasskeysSupport,
+ 'keyToggle': keyToggle,
+ 'keyDeleteConfirm': keyDeleteConfirm,
+ 'registration': registration,
+ }
+ }
+})();
\ No newline at end of file
diff --git a/passkeys/templates/PassKeys.html b/passkeys/templates/PassKeys.html
deleted file mode 100644
index 5feddf4..0000000
--- a/passkeys/templates/PassKeys.html
+++ /dev/null
@@ -1,152 +0,0 @@
-{% extends "PassKeys_base.html" %}
-{% load static %}
-{% block head %}
-{{block.super}}
-
-
-
-
-
-{% endblock %}
-{% block content %}
-{{block.super}}
-
-
-
- {% include "modal.html" %}
-{% endblock %}
diff --git a/passkeys/templates/check_passkeys.js b/passkeys/templates/check_passkeys.js
deleted file mode 100644
index 2241ddd..0000000
--- a/passkeys/templates/check_passkeys.js
+++ /dev/null
@@ -1,33 +0,0 @@
-function check_passkey(platform_authenticator = true,success_func, fail_func)
-{
- {% if request.session.passkey.cross_platform != False %}
- if (platform_authenticator)
- {
- PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
- .then((available) => {
- if (available) {
- success_func();
- }
- else{
- fail_func();
- }
- })
- }
- success_func();
- {% endif%}
-}
-
-function check_passkeys(success_func, fail_func)
-{
- PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
- .then((available) => {
- if (available) {
- success_func();
- } else {
- fail_func()
- }
- }).catch((err) => {
- // Something went wrong
- console.error(err);
- });
-}
\ No newline at end of file
diff --git a/passkeys/templates/modal.html b/passkeys/templates/modal.html
deleted file mode 100644
index 2e43f02..0000000
--- a/passkeys/templates/modal.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
\ No newline at end of file
diff --git a/passkeys/templates/passkeys.js b/passkeys/templates/passkeys.js
deleted file mode 100644
index d3c855f..0000000
--- a/passkeys/templates/passkeys.js
+++ /dev/null
@@ -1,79 +0,0 @@
-{% load static %}
-
-
-
\ No newline at end of file
diff --git a/passkeys/templates/passkeys/modal.html b/passkeys/templates/passkeys/modal.html
new file mode 100644
index 0000000..ce481b1
--- /dev/null
+++ b/passkeys/templates/passkeys/modal.html
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/passkeys/templates/passkeys/passkeys.html b/passkeys/templates/passkeys/passkeys.html
new file mode 100644
index 0000000..55f428f
--- /dev/null
+++ b/passkeys/templates/passkeys/passkeys.html
@@ -0,0 +1,69 @@
+{% extends "passkeys/passkeys_base.html" %}
+{% load static %}
+
+{% block head %}
+ {{ block.super }}
+
+
+
+ {% if enroll %}
+
+ {% endif %}
+
+
+{% endblock %}
+
+{% block content %}
+ {{ block.super }}
+
+
+
Your passkeys
+
+ Add Key
+
+
+
+
+
+ {% csrf_token %}
+ {% include "passkeys/modal.html" %}
+{% endblock %}
diff --git a/passkeys/templates/PassKeys_base.html b/passkeys/templates/passkeys/passkeys_base.html
similarity index 100%
rename from passkeys/templates/PassKeys_base.html
rename to passkeys/templates/passkeys/passkeys_base.html
diff --git a/passkeys/urls.py b/passkeys/urls.py
index eeba07b..519e42f 100644
--- a/passkeys/urls.py
+++ b/passkeys/urls.py
@@ -1,16 +1,19 @@
from django.urls import path
-from . import FIDO2,views
+from . import FIDO2, views
app_name = 'passkeys'
urlpatterns = [
- path('auth/begin',FIDO2.auth_begin, name='auth_begin'),
- path('auth/complete',FIDO2.auth_complete, name='auth_complete'),
- path('reg/begin',FIDO2.reg_begin,name = 'reg_begin'),
- path('reg/complete',FIDO2.reg_complete,name = 'reg_complete'),
- path('',views.index, name='home'),
- path('enroll/',views.index, name='enroll',kwargs={'enroll':True}),
+ # AUTHENTICATION
+ path('auth/begin/', FIDO2.auth_begin, name='auth_begin'),
+ path('auth/complete/', FIDO2.auth_complete, name='auth_complete'),
+ # REGISTRATION
+ path('reg/begin/', FIDO2.reg_begin, name='reg_begin'),
+ path('reg/complete/', FIDO2.reg_complete, name='reg_complete'),
- path('del/',views.delKey, name='delKey'),
- path('toggle/',views.toggleKey, name='toggle'),
- ]
\ No newline at end of file
+ # KEY MANAGEMENT
+ path('', views.index, name='home'),
+ path('enroll/', views.index, name='enroll', kwargs={'enroll': True}),
+ path('del/', views.delKey, name='delKey'),
+ path('toggle/', views.toggleKey, name='toggle'),
+]
diff --git a/passkeys/views.py b/passkeys/views.py
index d3d06f9..fe63888 100644
--- a/passkeys/views.py
+++ b/passkeys/views.py
@@ -1,26 +1,33 @@
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render
+from django.views.decorators.http import require_http_methods
from .models import UserPasskey
@login_required
def index(request,enroll=False): # noqa
keys = UserPasskey.objects.filter(user=request.user) # pragma: no cover
- return render(request,'PassKeys.html',{"keys":keys,"enroll":enroll}) # pragma: no cover
-
+ return render(request,'passkeys/passkeys.html',{"keys":keys,"enroll":enroll}) # pragma: no cover
+@require_http_methods(["POST"])
@login_required
def delKey(request):
- key=UserPasskey.objects.get(id=request.GET["id"])
- if key.user.pk == request.user.pk:
+ id=request.POST.get("id")
+ if not id:
+ return HttpResponse("Error: You are missing a key", status=403)
+ key=UserPasskey.objects.get(id=id)
+ if key.user.pk == request.user.pk:
key.delete()
return HttpResponse("Deleted Successfully")
return HttpResponse("Error: You own this token so you can't delete it", status=403)
+@require_http_methods(["POST"])
@login_required
def toggleKey(request):
- id=request.GET["id"]
+ id=request.POST.get("id")
+ if not id:
+ return HttpResponse("Error: You are missing a key", status=403)
q=UserPasskey.objects.filter(user=request.user, id=id)
if q.count()==1:
key=q[0]
diff --git a/setup.py b/setup.py
index 3007bbb..d135dea 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
setup(
name='django-passkeys',
- version='1.2.7',
+ version='2.0rc1',
description='A Django Authentication Backend for Passkeys',
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
@@ -18,7 +18,7 @@
'django >= 2.0',
'ua-parser',
'user-agents',
- 'fido2 == 1.1.1',
+ 'fido2 > 1.1.0',
],
python_requires=">=3.7",
include_package_data=True,
@@ -38,6 +38,7 @@
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
+ "Framework :: Django :: 5.1",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
@@ -47,6 +48,7 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
)