Repository: Onemind-Services-LLC/netbox-secrets (language: python)

Issue:
Update Filtersets and Filterforms with Test Cases
<!--
#########################################################################

Thank you for sharing your work and for opening a PR.

(!) IMPORTANT (!):
First make sure that you point your PR to the `dev` branch!

Now please read the comments carefully and try to provide information
on all relevant titles.

#########################################################################
-->
# Pull Request

## Related Issue

<!--
Add the related issue in the form of #issue-number (Example #100)
-->

## New Behavior

<!--
Please describe in a few words the intentions of your PR.
-->

---

## Contrast to Current Behavior

<!--
Please describe in a few words how the new behavior is different
from the current behavior.
-->

---

## Discussion: Benefits and Drawbacks

<!--
Please make your case here:

- Why do you think this project and the community will benefit from your
  proposed change?
- What are the drawbacks of this change?
- Is it backwards-compatible?
- Anything else that you think is relevant to the discussion of this PR.

(No need to write a huge article here. Just a few sentences that give some
additional context about the motivations for the change.)
-->

---

## Changes to the Documentation

<!--
If the docs must be updated, please include the changes in the PR.
If the Wiki must be updated, please make a suggestion below.
-->

---

## Double Check

<!--
Please put an x into the brackets (like `[x]`) if you've completed that task.
-->

* [ ] I have explained my PR according to the information in the comments
 or in a linked issue.
* [ ] My PR targets the `dev` branch.

[Bug]: Code key length mistake 4097 instead of 4096
### NetBox Secrets plugin version

v1.8.5

### NetBox version

v3.5.1

### Steps to Reproduce

A mistake in key length: 4097 instead of 4096.

'''
        if key_size not in range(2048, 4097, 256):
'''
https://github.com/Onemind-Services-LLC/netbox-secrets/blob/0b413174eae92dc47fcc56a5e135b0a24d50abe7/netbox_secrets/api/views.py#L279C40-L279C44

### Expected Behavior

Possible key length 256, 2048, 4096

### Observed Behavior

Possible key length 256, 2048, 409**_7_**
[Bug]: Unable to Activate Selected User Keys in Admin Panel
### NetBox Secrets plugin version

v1.9.0

### NetBox version

v3.6.4

### Steps to Reproduce

- The user creates a user key.
- The user recieves the " Status: Your user key is inactive. Ask an administrator to enable it for you."
- Enter with admin. Select the user to activate in (old) Admin Panel.
- After check the box, Select the "Activate selected user" error raises.
![Captura desde 2023-11-02 10-13-47](https://github.com/Onemind-Services-LLC/netbox-secrets/assets/32807381/8f833f34-d057-4b78-aaaf-cd65d3f63237)


### Expected Behavior

The user is active after the "Activate selected user" and Go button ocurrs

### Observed Behavior

Field 'id' expected a number but got ''. Seems like the checkbox has a missfunctionality
[Bug]: Button copy to clipboard not working
### NetBox Secrets plugin version

1.8.5

### NetBox version

3.5.9

### Steps to Reproduce

Unlock secret and then click Copy to clipboard. Bot the clipboard is empty.

### Expected Behavior

Secret gets copied to the clipboard.

### Observed Behavior

The event handler is missing:
![image](https://github.com/Onemind-Services-LLC/netbox-secrets/assets/22004345/e779d09f-2842-4320-a900-815ef832265c)



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6ccbb66..c5afbed 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,4 +58,4 @@ jobs:
 
       - id: docker-test
         name: Test the image
-        run: ./test.sh snapshot
+        run: ./test.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d413c30..46243e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,17 @@
 
 ## [Unreleased](https://github.com/Onemind-Services-LLC/netbox-secrets/tree/HEAD)
 
-[Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.8.5...HEAD)
+[Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.9.0...HEAD)
+
+**Closed issues:**
+
+- \[Bug\]: Unable to Activate Selected User Keys in Admin Panel [\#106](https://github.com/Onemind-Services-LLC/netbox-secrets/issues/106)
+- \[Bug\]: Code key length mistake 4097 instead of 4096 [\#92](https://github.com/Onemind-Services-LLC/netbox-secrets/issues/92)
+- \[Docs\]: API Pulling session Key [\#83](https://github.com/Onemind-Services-LLC/netbox-secrets/issues/83)
+
+## [v1.9.0](https://github.com/Onemind-Services-LLC/netbox-secrets/tree/v1.9.0) (2023-09-07)
+
+[Full Changelog](https://github.com/Onemind-Services-LLC/netbox-secrets/compare/v1.8.5...v1.9.0)
 
 **Closed issues:**
 
@@ -17,6 +27,8 @@
 
 **Merged pull requests:**
 
+- Prepare for release [\#98](https://github.com/Onemind-Services-LLC/netbox-secrets/pull/98) ([abhi1693](https://github.com/abhi1693))
+- Add support for NetBox v3.6 [\#97](https://github.com/Onemind-Services-LLC/netbox-secrets/pull/97) ([kprince28](https://github.com/kprince28))
 - Bump word-wrap from 1.2.3 to 1.2.4 in /netbox\_secrets/project-static [\#86](https://github.com/Onemind-Services-LLC/netbox-secrets/pull/86) ([dependabot[bot]](https://github.com/apps/dependabot))
 - Bump semver from 6.3.0 to 6.3.1 in /netbox\_secrets/project-static [\#84](https://github.com/Onemind-Services-LLC/netbox-secrets/pull/84) ([dependabot[bot]](https://github.com/apps/dependabot))
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 0b78731..4a1024e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,8 +5,6 @@ services:
     build:
       dockerfile: Dockerfile
       context: .
-      args:
-        NETBOX_VARIANT: ${NETBOX_VARIANT}
     depends_on:
       - postgres
       - redis
diff --git a/docs/rest-api/working-with-secrets.md b/docs/rest-api/working-with-secrets.md
index 786f17b..cfa30ec 100644
--- a/docs/rest-api/working-with-secrets.md
+++ b/docs/rest-api/working-with-secrets.md
@@ -7,24 +7,25 @@ steps are needed to encrypt or decrypt secret data.
 
 In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key,
 send an authenticated request to the `/api/plugins/secrets/session-keys/` endpoint with the private RSA key which
-matches your [UserKey](../models/userkey.md). The private key must be POSTed with the name `private_key`.
+matches your [UserKey](../models/userkey.md). Place the private RSA key in a json file.
 
 ```no-highlight
 $ curl -X POST http://netbox/api/plugins/secrets/session-keys/ \
 -H "Authorization: Token $TOKEN" \
 -H "Accept: application/json; indent=4" \
---data-urlencode "private_key@<filename>"
+-H "Content-Type: application/json" \
+--data @<filename>
 ```
 
 ```json
 {
     "pk": 7,
     "id": 7,
-    "url": "http://172.16.14.63:8000/api/plugins/secrets/session-keys/7/",
+    "url": "http://netbox/api/plugins/secrets/session-keys/7/",
     "display": "admin (RSA)",
     "userkey": {
         "id": 1,
-        "url": "http://172.16.14.63:8000/api/plugins/secrets/user-keys/1/",
+        "url": "http://netbox/api/plugins/secrets/user-keys/1/",
         "display": "admin"
     },
     "session_key": "4H8MCOl98qom7Ug5fQTzsFcH600SRWxe7KlUyIYxJ+A=",
@@ -34,11 +35,35 @@ $ curl -X POST http://netbox/api/plugins/secrets/session-keys/ \
 
 !!! note
     To read the private key from a file, use the convention above. Alternatively, the private key can be read from an
-environment variable using `--data-urlencode "private_key=$PRIVATE_KEY"`.
+environment variable using `--data "{\"private_key\": \"$PRIVATEKEY\"}"`.
+
+Use the following CLI command to convert your PEM RSA key to json:
+
+```no-highlight
+jq -sR . <filename>
+```
 
 The request uses the provided private key to unlock your stored copy of the master key and generate a temporary
 session key, which can be attached in the `X-Session-Key` header of future API requests.
 
+### Depracated!
+
+If you still want to use `application/x-www-form-urlencoded` you can use the **depracated** API endpoint
+`http://netbox/api/plugins/secrets/get-session-key/`.
+
+```no-highlight
+curl -X POST https://netbox-test.tugraz.at/api/plugins/secrets/get-session-key/ \
+-H "Authorization: Token $TOKEN" \
+-H "Accept: application/json; indent=4" \
+--data-urlencode "private_key@<filename>" 
+```
+
+```json
+{
+    "session_key": "4H8MCOl98qom7Ug5fQTzsFcH600SRWxe7KlUyIYxJ+A="
+}
+```
+
 ## Retrieving Secrets
 
 A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its
diff --git a/netbox_secrets/admin.py b/netbox_secrets/admin.py
index 981fb5a..e92174d 100644
--- a/netbox_secrets/admin.py
+++ b/netbox_secrets/admin.py
@@ -13,21 +13,21 @@ class UserKeyAdmin(admin.ModelAdmin):
     fields = ['user', 'public_key', 'is_active', 'last_updated']
     readonly_fields = ['user', 'is_active', 'last_updated']
 
+    def has_add_permission(self, request):
+        # Don't allow a user to create a new public key directly.
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        # Don't allow a user to delete a public key directly.
+        return False
+
     def get_readonly_fields(self, request, obj=None):
         # Don't allow a user to modify an existing public key directly.
         if obj and obj.public_key:
             return ['public_key'] + self.readonly_fields
         return self.readonly_fields
 
-    def get_actions(self, request):
-        # Bulk deletion is disabled at the manager level, so remove the action from the admin site for this model.
-        actions = super().get_actions(request)
-        if 'delete_selected' in actions:
-            del actions['delete_selected']
-        if not request.user.has_perm('secrets.change_userkey'):
-            del actions['activate_selected']
-        return actions
-
+    @admin.action(description='Activate selected public keys', permissions=['change'])
     def activate_selected(self, request, queryset):
         """
         Enable bulk activation of UserKeys
@@ -35,7 +35,11 @@ def activate_selected(self, request, queryset):
         try:
             my_userkey = UserKey.objects.get(user=request.user)
         except UserKey.DoesNotExist:
-            messages.error(request, "You do not have an active User Key.")
+            messages.error(request, "You do not have a User Key.")
+            return redirect('admin:netbox_secrets_userkey_changelist')
+
+        if not my_userkey.is_active():
+            messages.error(request, "Your User Key is not active.")
             return redirect('admin:netbox_secrets_userkey_changelist')
 
         if 'activate' in request.POST:
@@ -43,8 +47,12 @@ def activate_selected(self, request, queryset):
             if form.is_valid():
                 master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
                 if master_key is not None:
-                    for uk in form.cleaned_data['_selected_action']:
+                    for uk in form.cleaned_data[ACTION_CHECKBOX_NAME]:
                         uk.activate(master_key)
+                    messages.success(
+                        request,
+                        "Successfully activated {} user keys.".format(len(form.cleaned_data[ACTION_CHECKBOX_NAME])),
+                    )
                     return redirect('admin:netbox_secrets_userkey_changelist')
                 else:
                     messages.error(
@@ -53,7 +61,7 @@ def activate_selected(self, request, queryset):
                         extra_tags='error',
                     )
         else:
-            form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(ACTION_CHECKBOX_NAME)})
+            form = ActivateUserKeyForm(initial={ACTION_CHECKBOX_NAME: request.POST.getlist(ACTION_CHECKBOX_NAME)})
 
         return render(
             request,
@@ -63,4 +71,3 @@ def activate_selected(self, request, queryset):
             },
         )
 
-    activate_selected.short_description = "Activate selected user keys"
diff --git a/netbox_secrets/api/views.py b/netbox_secrets/api/views.py
index 38d4297..444861a 100644
--- a/netbox_secrets/api/views.py
+++ b/netbox_secrets/api/views.py
@@ -276,7 +276,7 @@ def list(self, request):
         except ValueError:
             key_size = public_key_size
 
-        if key_size not in range(2048, 4097, 256):
+        if key_size not in range(2048, 8193, 256):
             key_size = public_key_size
 
         # Export RSA private and public keys in PEM format
diff --git a/netbox_secrets/filtersets.py b/netbox_secrets/filtersets.py
index 1410665..e3109bc 100644
--- a/netbox_secrets/filtersets.py
+++ b/netbox_secrets/filtersets.py
@@ -19,15 +19,11 @@
 
 
 class SecretRoleFilterSet(NetBoxModelFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
     name = MultiValueCharFilter(lookup_expr='iexact')
 
     class Meta:
         model = SecretRole
-        fields = ['id', 'name', 'slug', 'description', 'comments']
+        fields = ['id', 'slug', 'description', 'comments']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -43,13 +39,6 @@ def search(self, queryset, name, value):
 if plugin_settings.get('enable_contacts', False):
 
     class SecretFilterSet(NetBoxModelFilterSet):
-        q = django_filters.CharFilter(
-            method='search',
-            label='Search',
-        )
-
-        name = MultiValueCharFilter(lookup_expr='iexact')
-
         role_id = django_filters.ModelMultipleChoiceFilter(
             queryset=SecretRole.objects.all(),
             label='Role (ID)',
@@ -78,12 +67,6 @@ class Meta:
             model = Secret
             fields = [
                 'id',
-                'assigned_object_type',
-                'assigned_object_type_id',
-                'assigned_object_id',
-                'role_id',
-                'role',
-                'name',
                 'contact',
                 'description',
                 'comments',
@@ -103,13 +86,9 @@ def search(self, queryset, name, value):
 else:
 
     class SecretFilterSet(NetBoxModelFilterSet):
-        q = django_filters.CharFilter(
-            method='search',
-            label='Search',
+        name = MultiValueCharFilter(
+            lookup_expr='iexact'
         )
-
-        name = MultiValueCharFilter(lookup_expr='iexact')
-
         role_id = django_filters.ModelMultipleChoiceFilter(
             queryset=SecretRole.objects.all(),
             label='Role (ID)',
@@ -132,12 +111,6 @@ class Meta:
             model = Secret
             fields = [
                 'id',
-                'assigned_object_type',
-                'assigned_object_type_id',
-                'assigned_object_id',
-                'role_id',
-                'role',
-                'name',
                 '_object_repr',
                 'description',
                 'comments',
diff --git a/netbox_secrets/forms/filterset.py b/netbox_secrets/forms/filterset.py
index 5842c36..5176900 100644
--- a/netbox_secrets/forms/filterset.py
+++ b/netbox_secrets/forms/filterset.py
@@ -19,8 +19,12 @@
 
 class SecretRoleFilterForm(NetBoxModelFilterSetForm):
     model = SecretRole
-    q = forms.CharField(required=False, label=_('Search'))
-    id = DynamicModelMultipleChoiceField(queryset=SecretRole.objects.all(), required=False, label=_('Roles'))
+    fieldsets = (
+        (None, ('q', 'filter_id', 'tag')),
+        ('Secret Role', ('id',))
+    )
+    id = DynamicModelMultipleChoiceField(queryset=SecretRole.objects.all(), required=False, label=_('Roles Name'))
+    tag = TagFilterField(model)
 
 
 class SecretFilterForm(NetBoxModelFilterSetForm):
@@ -28,10 +32,15 @@ class SecretFilterForm(NetBoxModelFilterSetForm):
 
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
+        ('Secret', ('id',)),
         ('Attributes', ('role_id', 'assigned_object_type_id')),
     )
 
-    q = forms.CharField(required=False, label=_('Search'))
+    id = DynamicModelMultipleChoiceField(
+        queryset=Secret.objects.all(),
+        required=False,
+        label=_('Name')
+    )
     assigned_object_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.filter(SECRET_ASSIGNABLE_MODELS),
         required=False,
diff --git a/netbox_secrets/models/secrets.py b/netbox_secrets/models/secrets.py
index 77d52bd..17d1175 100644
--- a/netbox_secrets/models/secrets.py
+++ b/netbox_secrets/models/secrets.py
@@ -87,10 +87,10 @@ def clean(self):
                     },
                 )
             # We can't use keys bigger than our master_key_cipher field can hold
-            if pubkey_length > 4096:
+            if pubkey_length > 8192:
                 raise ValidationError(
                     {
-                        'public_key': "Public key size ({}) is too large. Maximum key size is 4096 bits.".format(
+                        'public_key': "Public key size ({}) is too large. Maximum key size is 8192 bits.".format(
                             pubkey_length,
                         ),
                     },
diff --git a/netbox_secrets/navigation.py b/netbox_secrets/navigation.py
index 7aa1f46..b15af8c 100644
--- a/netbox_secrets/navigation.py
+++ b/netbox_secrets/navigation.py
@@ -1,7 +1,10 @@
-from extras.plugins import PluginMenuButton, PluginMenuItem
+from django.conf import settings
+from extras.plugins import PluginMenuButton, PluginMenuItem, PluginMenu
 from utilities.choices import ButtonColorChoices
 
-menu_items = (
+plugins_settings = settings.PLUGINS_CONFIG.get('netbox_secrets')
+
+menu_buttons = (
     PluginMenuItem(
         link_text="User Key",
         link="plugins:netbox_secrets:userkey",
@@ -34,3 +37,12 @@
         permissions=["netbox_secrets.view_secret"],
     ),
 )
+
+if plugins_settings.get('top_level_menu'):
+    menu = PluginMenu(
+        label='Secrets',
+        groups=(('Secrets', menu_buttons),),
+        icon_class='mdi mdi-eye-closed',
+    )
+else:
+    menu_items = menu_buttons
diff --git a/netbox_secrets/project-static/src/secrets.ts b/netbox_secrets/project-static/src/secrets.ts
index a17b7f4..daed7b1 100644
--- a/netbox_secrets/project-static/src/secrets.ts
+++ b/netbox_secrets/project-static/src/secrets.ts
@@ -66,7 +66,7 @@ function initGenerateKeyPair() {
 function toggleSecretButtons(id: string, action: 'lock' | 'unlock') {
   const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`);
   const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`);
-  const copyButton = document.querySelector(`button.copy-secret[secret-id='${id}']`);
+  const copyButton = document.querySelector(`span[secret-id='${id}']`);
   // If we're unlocking, hide the unlock button. Otherwise, show it.
   if (unlockButton !== null) {
     if (action === 'unlock') unlockButton.classList.add('d-none');
diff --git a/netbox_secrets/static/netbox_secrets/secrets.js b/netbox_secrets/static/netbox_secrets/secrets.js
index 62a62e5..424c14e 100644
--- a/netbox_secrets/static/netbox_secrets/secrets.js
+++ b/netbox_secrets/static/netbox_secrets/secrets.js
@@ -1,5 +1,5 @@
-"use strict";(()=>{var P=Object.create;var b=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,D=Object.prototype.hasOwnProperty;var _=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var H=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of R(r))!D.call(e,i)&&i!==o&&b(e,i,{get:()=>r[i],enumerable:!(t=M(r,i))||t.enumerable});return e};var B=(e,r,o)=>(o=e!=null?P(C(e)):{},H(r||!e||!e.__esModule?b(o,"default",{value:e,enumerable:!0}):o,e));var p=(e,r,o)=>new Promise((t,i)=>{var s=c=>{try{a(o.next(c))}catch(l){i(l)}},n=c=>{try{a(o.throw(c))}catch(l){i(l)}},a=c=>c.done?t(c.value):Promise.resolve(c.value).then(s,n);a((o=o.apply(e,r)).next())});var h=_(E=>{"use strict";E.parse=O;E.serialize=q;var I=Object.prototype.toString,m=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function O(e,r){if(typeof e!="string")throw new TypeError("argument str must be a string");for(var o={},t=r||{},i=t.decode||$,s=0;s<e.length;){var n=e.indexOf("=",s);if(n===-1)break;var a=e.indexOf(";",s);if(a===-1)a=e.length;else if(a<n){s=e.lastIndexOf(";",n-1)+1;continue}var c=e.slice(s,n).trim();if(o[c]===void 0){var l=e.slice(n+1,a).trim();l.charCodeAt(0)===34&&(l=l.slice(1,-1)),o[c]=F(l,i)}s=a+1}return o}function q(e,r,o){var t=o||{},i=t.encode||K;if(typeof i!="function")throw new TypeError("option encode is invalid");if(!m.test(e))throw new TypeError("argument name is invalid");var s=i(r);if(s&&!m.test(s))throw new TypeError("argument val is invalid");var n=e+"="+s;if(t.maxAge!=null){var a=t.maxAge-0;if(isNaN(a)||!isFinite(a))throw new TypeError("option maxAge is invalid");n+="; Max-Age="+Math.floor(a)}if(t.domain){if(!m.test(t.domain))throw new TypeError("option domain is invalid");n+="; Domain="+t.domain}if(t.path){if(!m.test(t.path))throw new TypeError("option path is invalid");n+="; Path="+t.path}if(t.expires){var c=t.expires;if(!j(c)||isNaN(c.valueOf()))throw new TypeError("option expires is invalid");n+="; Expires="+c.toUTCString()}if(t.httpOnly&&(n+="; HttpOnly"),t.secure&&(n+="; Secure"),t.priority){var l=typeof t.priority=="string"?t.priority.toLowerCase():t.priority;switch(l){case"low":n+="; Priority=Low";break;case"medium":n+="; Priority=Medium";break;case"high":n+="; Priority=High";break;default:throw new TypeError("option priority is invalid")}}if(t.sameSite){var u=typeof t.sameSite=="string"?t.sameSite.toLowerCase():t.sameSite;switch(u){case!0:n+="; SameSite=Strict";break;case"lax":n+="; SameSite=Lax";break;case"strict":n+="; SameSite=Strict";break;case"none":n+="; SameSite=None";break;default:throw new TypeError("option sameSite is invalid")}}return n}function $(e){return e.indexOf("%")!==-1?decodeURIComponent(e):e}function K(e){return encodeURIComponent(e)}function j(e){return I.call(e)==="[object Date]"||e instanceof Date}function F(e,r){try{return r(e)}catch(o){return e}}});function d(e,r,o,t){let i="mdi-alert";switch(e){case"warning":i="mdi-alert";break;case"success":i="mdi-check-circle";break;case"info":i="mdi-information";break;case"danger":i="mdi-alert";break}let s=document.createElement("div");s.setAttribute("class","toast-container position-fixed bottom-0 end-0 m-3");let n=document.createElement("div");n.setAttribute("class",`toast bg-${e}`),n.setAttribute("role","alert"),n.setAttribute("aria-live","assertive"),n.setAttribute("aria-atomic","true");let a=document.createElement("div");a.setAttribute("class",`toast-header bg-${e} text-body`);let c=document.createElement("i");c.setAttribute("class",`mdi ${i}`);let l=document.createElement("strong");l.setAttribute("class","me-auto ms-1"),l.innerText=r;let u=document.createElement("button");u.setAttribute("type","button"),u.setAttribute("class","btn-close"),u.setAttribute("data-bs-dismiss","toast"),u.setAttribute("aria-label","Close");let y=document.createElement("div");if(y.setAttribute("class","toast-body"),a.appendChild(c),a.appendChild(l),typeof t!="undefined"){let g=document.createElement("small");g.setAttribute("class","text-muted"),a.appendChild(g)}return a.appendChild(u),y.innerText=o.trim(),n.appendChild(a),n.appendChild(y),s.appendChild(n),document.body.appendChild(s),new window.Toast(n)}var T=B(h());function A(e){return"error"in e&&"exception"in e}function f(e){return"error"in e}function v(e){return"value"in e&&"required"in e}function N(){let{csrftoken:e}=T.default.parse(document.cookie);if(typeof e=="undefined")throw new Error("Invalid or missing CSRF token");return e}function S(e,r,o){return p(this,null,function*(){let t=N(),i=new Headers({"X-CSRFToken":t}),s;typeof o!="undefined"&&(s=JSON.stringify(o),i.set("content-type","application/json"),i.set("Accept","application/json"));let n=yield fetch(e,{method:r,body:s,headers:i,credentials:"same-origin"}),a=n.headers.get("Content-Type");if(typeof a=="string"&&a.includes("text"))return{error:yield n.text()};let c=yield n.json();return!n.ok&&Array.isArray(c)?{error:c.join(`
-`)}:!n.ok&&"detail"in c?{error:c.detail}:c})}function x(e,r){return p(this,null,function*(){return yield S(e,"POST",r)})}function k(e){return p(this,null,function*(){return yield S(e,"GET")})}function G(){let e=document.getElementById("new_keypair_modal"),r=document.getElementById("use_new_pubkey");if(e===null||r===null)return;let o=e.querySelector("textarea#new_pubkey"),t=e.querySelector("textarea#new_privkey");function i(){for(let n of[o,t])n!==null&&n.setAttribute("readonly","");k("/api/plugins/secrets/generate-rsa-key-pair/").then(n=>{if(f(n))d("danger","Error",n.error).show();else{let{private_key:a,public_key:c}=n;o!==null&&t!==null&&(o.value=c,t.value=a)}})}function s(){let n=document.getElementById("id_public_key");o!==null&&(n.value=o.value,n.innerText=o.value)}e.addEventListener("shown.bs.modal",()=>i()),r.addEventListener("click",()=>s())}function L(e,r){let o=document.querySelector(`button.unlock-secret[secret-id='${e}']`),t=document.querySelector(`button.lock-secret[secret-id='${e}']`),i=document.querySelector(`button.copy-secret[secret-id='${e}']`);o!==null&&(r==="unlock"&&o.classList.add("d-none"),r==="lock"&&o.classList.remove("d-none")),t!==null&&(r==="unlock"&&t.classList.remove("d-none"),r==="lock"&&t.classList.add("d-none")),i!==null&&(r==="unlock"&&i.classList.remove("d-none"),r==="lock"&&i.classList.add("d-none"))}function U(){let e=new window.Modal("#privkey_modal");function r(t){let i=document.getElementById(`secret_${t}`);typeof t=="string"&&t!==""&&k(`/api/plugins/secrets/secrets/${t}/`).then(s=>{if(f(s))s.error.toLowerCase().includes("invalid session key")?e.show():d("danger","Error",s.error).show();else{let{plaintext:n}=s;i!==null&&n!==null?(v(i)?i.value=n:i.innerText=n,L(t,"unlock")):e.show()}})}function o(t){if(typeof t=="string"&&t!==""){let i=document.getElementById(`secret_${t}`);v(i)?i.value="********":i.innerText="********",L(t,"lock")}}for(let t of document.querySelectorAll("button.unlock-secret"))t.addEventListener("click",()=>r(t.getAttribute("secret-id")));for(let t of document.querySelectorAll("button.lock-secret"))t.addEventListener("click",()=>o(t.getAttribute("secret-id")))}function z(e){x("/api/plugins/secrets/session-keys/",{private_key:e,preserve_key:!0}).then(r=>{if(!f(r))window.location.pathname==="/plugins/secrets/user-key/"?window.location.reload():d("success","Session Key Received","You may now unlock secrets.").show();else{let o=r.error;A(r)&&(o+=`
+"use strict";(()=>{var P=Object.create;var h=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,D=Object.prototype.hasOwnProperty;var _=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var H=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of R(r))!D.call(e,i)&&i!==o&&h(e,i,{get:()=>r[i],enumerable:!(t=M(r,i))||t.enumerable});return e};var B=(e,r,o)=>(o=e!=null?P(C(e)):{},H(r||!e||!e.__esModule?h(o,"default",{value:e,enumerable:!0}):o,e));var p=(e,r,o)=>new Promise((t,i)=>{var s=c=>{try{a(o.next(c))}catch(l){i(l)}},n=c=>{try{a(o.throw(c))}catch(l){i(l)}},a=c=>c.done?t(c.value):Promise.resolve(c.value).then(s,n);a((o=o.apply(e,r)).next())});var b=_(E=>{"use strict";E.parse=O;E.serialize=q;var I=Object.prototype.toString,m=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function O(e,r){if(typeof e!="string")throw new TypeError("argument str must be a string");for(var o={},t=r||{},i=t.decode||$,s=0;s<e.length;){var n=e.indexOf("=",s);if(n===-1)break;var a=e.indexOf(";",s);if(a===-1)a=e.length;else if(a<n){s=e.lastIndexOf(";",n-1)+1;continue}var c=e.slice(s,n).trim();if(o[c]===void 0){var l=e.slice(n+1,a).trim();l.charCodeAt(0)===34&&(l=l.slice(1,-1)),o[c]=F(l,i)}s=a+1}return o}function q(e,r,o){var t=o||{},i=t.encode||K;if(typeof i!="function")throw new TypeError("option encode is invalid");if(!m.test(e))throw new TypeError("argument name is invalid");var s=i(r);if(s&&!m.test(s))throw new TypeError("argument val is invalid");var n=e+"="+s;if(t.maxAge!=null){var a=t.maxAge-0;if(isNaN(a)||!isFinite(a))throw new TypeError("option maxAge is invalid");n+="; Max-Age="+Math.floor(a)}if(t.domain){if(!m.test(t.domain))throw new TypeError("option domain is invalid");n+="; Domain="+t.domain}if(t.path){if(!m.test(t.path))throw new TypeError("option path is invalid");n+="; Path="+t.path}if(t.expires){var c=t.expires;if(!j(c)||isNaN(c.valueOf()))throw new TypeError("option expires is invalid");n+="; Expires="+c.toUTCString()}if(t.httpOnly&&(n+="; HttpOnly"),t.secure&&(n+="; Secure"),t.priority){var l=typeof t.priority=="string"?t.priority.toLowerCase():t.priority;switch(l){case"low":n+="; Priority=Low";break;case"medium":n+="; Priority=Medium";break;case"high":n+="; Priority=High";break;default:throw new TypeError("option priority is invalid")}}if(t.sameSite){var u=typeof t.sameSite=="string"?t.sameSite.toLowerCase():t.sameSite;switch(u){case!0:n+="; SameSite=Strict";break;case"lax":n+="; SameSite=Lax";break;case"strict":n+="; SameSite=Strict";break;case"none":n+="; SameSite=None";break;default:throw new TypeError("option sameSite is invalid")}}return n}function $(e){return e.indexOf("%")!==-1?decodeURIComponent(e):e}function K(e){return encodeURIComponent(e)}function j(e){return I.call(e)==="[object Date]"||e instanceof Date}function F(e,r){try{return r(e)}catch(o){return e}}});function d(e,r,o,t){let i="mdi-alert";switch(e){case"warning":i="mdi-alert";break;case"success":i="mdi-check-circle";break;case"info":i="mdi-information";break;case"danger":i="mdi-alert";break}let s=document.createElement("div");s.setAttribute("class","toast-container position-fixed bottom-0 end-0 m-3");let n=document.createElement("div");n.setAttribute("class",`toast bg-${e}`),n.setAttribute("role","alert"),n.setAttribute("aria-live","assertive"),n.setAttribute("aria-atomic","true");let a=document.createElement("div");a.setAttribute("class",`toast-header bg-${e} text-body`);let c=document.createElement("i");c.setAttribute("class",`mdi ${i}`);let l=document.createElement("strong");l.setAttribute("class","me-auto ms-1"),l.innerText=r;let u=document.createElement("button");u.setAttribute("type","button"),u.setAttribute("class","btn-close"),u.setAttribute("data-bs-dismiss","toast"),u.setAttribute("aria-label","Close");let y=document.createElement("div");if(y.setAttribute("class","toast-body"),a.appendChild(c),a.appendChild(l),typeof t!="undefined"){let g=document.createElement("small");g.setAttribute("class","text-muted"),a.appendChild(g)}return a.appendChild(u),y.innerText=o.trim(),n.appendChild(a),n.appendChild(y),s.appendChild(n),document.body.appendChild(s),new window.Toast(n)}var T=B(b());function A(e){return"error"in e&&"exception"in e}function f(e){return"error"in e}function v(e){return"value"in e&&"required"in e}function N(){let{csrftoken:e}=T.default.parse(document.cookie);if(typeof e=="undefined")throw new Error("Invalid or missing CSRF token");return e}function S(e,r,o){return p(this,null,function*(){let t=N(),i=new Headers({"X-CSRFToken":t}),s;typeof o!="undefined"&&(s=JSON.stringify(o),i.set("content-type","application/json"),i.set("Accept","application/json"));let n=yield fetch(e,{method:r,body:s,headers:i,credentials:"same-origin"}),a=n.headers.get("Content-Type");if(typeof a=="string"&&a.includes("text"))return{error:yield n.text()};let c=yield n.json();return!n.ok&&Array.isArray(c)?{error:c.join(`
+`)}:!n.ok&&"detail"in c?{error:c.detail}:c})}function x(e,r){return p(this,null,function*(){return yield S(e,"POST",r)})}function k(e){return p(this,null,function*(){return yield S(e,"GET")})}function G(){let e=document.getElementById("new_keypair_modal"),r=document.getElementById("use_new_pubkey");if(e===null||r===null)return;let o=e.querySelector("textarea#new_pubkey"),t=e.querySelector("textarea#new_privkey");function i(){for(let n of[o,t])n!==null&&n.setAttribute("readonly","");k("/api/plugins/secrets/generate-rsa-key-pair/").then(n=>{if(f(n))d("danger","Error",n.error).show();else{let{private_key:a,public_key:c}=n;o!==null&&t!==null&&(o.value=c,t.value=a)}})}function s(){let n=document.getElementById("id_public_key");o!==null&&(n.value=o.value,n.innerText=o.value)}e.addEventListener("shown.bs.modal",()=>i()),r.addEventListener("click",()=>s())}function L(e,r){let o=document.querySelector(`button.unlock-secret[secret-id='${e}']`),t=document.querySelector(`button.lock-secret[secret-id='${e}']`),i=document.querySelector(`span[secret-id='${e}']`);console.log(i),o!==null&&(r==="unlock"&&o.classList.add("d-none"),r==="lock"&&o.classList.remove("d-none")),t!==null&&(r==="unlock"&&t.classList.remove("d-none"),r==="lock"&&t.classList.add("d-none")),i!==null&&(r==="unlock"&&i.classList.remove("d-none"),r==="lock"&&i.classList.add("d-none"))}function U(){let e=new window.Modal("#privkey_modal");function r(t){let i=document.getElementById(`secret_${t}`);typeof t=="string"&&t!==""&&k(`/api/plugins/secrets/secrets/${t}/`).then(s=>{if(f(s))s.error.toLowerCase().includes("invalid session key")?e.show():d("danger","Error",s.error).show();else{let{plaintext:n}=s;i!==null&&n!==null?(v(i)?i.value=n:i.innerText=n,L(t,"unlock")):e.show()}})}function o(t){if(typeof t=="string"&&t!==""){let i=document.getElementById(`secret_${t}`);v(i)?i.value="********":i.innerText="********",L(t,"lock")}}for(let t of document.querySelectorAll("button.unlock-secret"))t.addEventListener("click",()=>r(t.getAttribute("secret-id")));for(let t of document.querySelectorAll("button.lock-secret"))t.addEventListener("click",()=>o(t.getAttribute("secret-id")))}function z(e){x("/api/plugins/secrets/session-keys/",{private_key:e,preserve_key:!0}).then(r=>{if(!f(r))window.location.pathname==="/plugins/secrets/user-key/"?window.location.reload():d("success","Session Key Received","You may now unlock secrets.").show();else{let o=r.error;A(r)&&(o+=`
 ${r.exception}`),d("danger","Failed to Retrieve Session Key",o).show()}})}function J(){for(let r of document.querySelectorAll("#request_session_key")){let o=function(){for(let t of document.querySelectorAll("#user_privkey"))z(t.value),t.value=""};var e=o;r.addEventListener("click",o)}}function X(){let e=new window.Modal("#privkey_modal");function r(o){document.cookie.indexOf("netbox_secrets_sessionid")===-1&&(o.preventDefault(),e.show())}for(let o of document.querySelectorAll(".requires-session-key")){let t=o.closest("form");t!==null&&t.addEventListener("submit",r)}}function w(){for(let e of[G,U,J,X])e()}document.readyState!=="loading"?w():document.addEventListener("DOMContentLoaded",w);})();
 /*! Bundled license information:
 
diff --git a/netbox_secrets/static/netbox_secrets/secrets.js.map b/netbox_secrets/static/netbox_secrets/secrets.js.map
index c595b5a..cedd1d7 100644
--- a/netbox_secrets/static/netbox_secrets/secrets.js.map
+++ b/netbox_secrets/static/netbox_secrets/secrets.js.map
@@ -1,7 +1,7 @@
 {
   "version": 3,
   "sources": ["../node_modules/cookie/index.js", "../src/bs.ts", "../src/util.ts", "../src/secrets.ts", "../src/index.ts"],
-  "sourcesContent": ["/*!\n * cookie\n * Copyright(c) 2012-2014 Roman Shtylman\n * Copyright(c) 2015 Douglas Christopher Wilson\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module exports.\n * @public\n */\n\nexports.parse = parse;\nexports.serialize = serialize;\n\n/**\n * Module variables.\n * @private\n */\n\nvar __toString = Object.prototype.toString\n\n/**\n * RegExp to match field-content in RFC 7230 sec 3.2\n *\n * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]\n * field-vchar   = VCHAR / obs-text\n * obs-text      = %x80-FF\n */\n\nvar fieldContentRegExp = /^[\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+$/;\n\n/**\n * Parse a cookie header.\n *\n * Parse the given cookie header string into an object\n * The object has the various cookies as keys(names) => values\n *\n * @param {string} str\n * @param {object} [options]\n * @return {object}\n * @public\n */\n\nfunction parse(str, options) {\n  if (typeof str !== 'string') {\n    throw new TypeError('argument str must be a string');\n  }\n\n  var obj = {}\n  var opt = options || {};\n  var dec = opt.decode || decode;\n\n  var index = 0\n  while (index < str.length) {\n    var eqIdx = str.indexOf('=', index)\n\n    // no more cookie pairs\n    if (eqIdx === -1) {\n      break\n    }\n\n    var endIdx = str.indexOf(';', index)\n\n    if (endIdx === -1) {\n      endIdx = str.length\n    } else if (endIdx < eqIdx) {\n      // backtrack on prior semicolon\n      index = str.lastIndexOf(';', eqIdx - 1) + 1\n      continue\n    }\n\n    var key = str.slice(index, eqIdx).trim()\n\n    // only assign once\n    if (undefined === obj[key]) {\n      var val = str.slice(eqIdx + 1, endIdx).trim()\n\n      // quoted values\n      if (val.charCodeAt(0) === 0x22) {\n        val = val.slice(1, -1)\n      }\n\n      obj[key] = tryDecode(val, dec);\n    }\n\n    index = endIdx + 1\n  }\n\n  return obj;\n}\n\n/**\n * Serialize data into a cookie header.\n *\n * Serialize the a name value pair into a cookie string suitable for\n * http headers. An optional options object specified cookie parameters.\n *\n * serialize('foo', 'bar', { httpOnly: true })\n *   => \"foo=bar; httpOnly\"\n *\n * @param {string} name\n * @param {string} val\n * @param {object} [options]\n * @return {string}\n * @public\n */\n\nfunction serialize(name, val, options) {\n  var opt = options || {};\n  var enc = opt.encode || encode;\n\n  if (typeof enc !== 'function') {\n    throw new TypeError('option encode is invalid');\n  }\n\n  if (!fieldContentRegExp.test(name)) {\n    throw new TypeError('argument name is invalid');\n  }\n\n  var value = enc(val);\n\n  if (value && !fieldContentRegExp.test(value)) {\n    throw new TypeError('argument val is invalid');\n  }\n\n  var str = name + '=' + value;\n\n  if (null != opt.maxAge) {\n    var maxAge = opt.maxAge - 0;\n\n    if (isNaN(maxAge) || !isFinite(maxAge)) {\n      throw new TypeError('option maxAge is invalid')\n    }\n\n    str += '; Max-Age=' + Math.floor(maxAge);\n  }\n\n  if (opt.domain) {\n    if (!fieldContentRegExp.test(opt.domain)) {\n      throw new TypeError('option domain is invalid');\n    }\n\n    str += '; Domain=' + opt.domain;\n  }\n\n  if (opt.path) {\n    if (!fieldContentRegExp.test(opt.path)) {\n      throw new TypeError('option path is invalid');\n    }\n\n    str += '; Path=' + opt.path;\n  }\n\n  if (opt.expires) {\n    var expires = opt.expires\n\n    if (!isDate(expires) || isNaN(expires.valueOf())) {\n      throw new TypeError('option expires is invalid');\n    }\n\n    str += '; Expires=' + expires.toUTCString()\n  }\n\n  if (opt.httpOnly) {\n    str += '; HttpOnly';\n  }\n\n  if (opt.secure) {\n    str += '; Secure';\n  }\n\n  if (opt.priority) {\n    var priority = typeof opt.priority === 'string'\n      ? opt.priority.toLowerCase()\n      : opt.priority\n\n    switch (priority) {\n      case 'low':\n        str += '; Priority=Low'\n        break\n      case 'medium':\n        str += '; Priority=Medium'\n        break\n      case 'high':\n        str += '; Priority=High'\n        break\n      default:\n        throw new TypeError('option priority is invalid')\n    }\n  }\n\n  if (opt.sameSite) {\n    var sameSite = typeof opt.sameSite === 'string'\n      ? opt.sameSite.toLowerCase() : opt.sameSite;\n\n    switch (sameSite) {\n      case true:\n        str += '; SameSite=Strict';\n        break;\n      case 'lax':\n        str += '; SameSite=Lax';\n        break;\n      case 'strict':\n        str += '; SameSite=Strict';\n        break;\n      case 'none':\n        str += '; SameSite=None';\n        break;\n      default:\n        throw new TypeError('option sameSite is invalid');\n    }\n  }\n\n  return str;\n}\n\n/**\n * URL-decode string value. Optimized to skip native call when no %.\n *\n * @param {string} str\n * @returns {string}\n */\n\nfunction decode (str) {\n  return str.indexOf('%') !== -1\n    ? decodeURIComponent(str)\n    : str\n}\n\n/**\n * URL-encode value.\n *\n * @param {string} str\n * @returns {string}\n */\n\nfunction encode (val) {\n  return encodeURIComponent(val)\n}\n\n/**\n * Determine if value is a Date.\n *\n * @param {*} val\n * @private\n */\n\nfunction isDate (val) {\n  return __toString.call(val) === '[object Date]' ||\n    val instanceof Date\n}\n\n/**\n * Try decoding a string using a decoding function.\n *\n * @param {string} str\n * @param {function} decode\n * @private\n */\n\nfunction tryDecode(str, decode) {\n  try {\n    return decode(str);\n  } catch (e) {\n    return str;\n  }\n}\n", "type ToastLevel = 'danger' | 'warning' | 'success' | 'info';\n\nexport function createToast(\n  level: ToastLevel,\n  title: string,\n  message: string,\n  extra?: string,\n): InstanceType<typeof window.Toast> {\n  let iconName = 'mdi-alert';\n  switch (level) {\n    case 'warning':\n      iconName = 'mdi-alert';\n      break;\n    case 'success':\n      iconName = 'mdi-check-circle';\n      break;\n    case 'info':\n      iconName = 'mdi-information';\n      break;\n    case 'danger':\n      iconName = 'mdi-alert';\n      break;\n  }\n\n  const container = document.createElement('div');\n  container.setAttribute('class', 'toast-container position-fixed bottom-0 end-0 m-3');\n\n  const main = document.createElement('div');\n  main.setAttribute('class', `toast bg-${level}`);\n  main.setAttribute('role', 'alert');\n  main.setAttribute('aria-live', 'assertive');\n  main.setAttribute('aria-atomic', 'true');\n\n  const header = document.createElement('div');\n  header.setAttribute('class', `toast-header bg-${level} text-body`);\n\n  const icon = document.createElement('i');\n  icon.setAttribute('class', `mdi ${iconName}`);\n\n  const titleElement = document.createElement('strong');\n  titleElement.setAttribute('class', 'me-auto ms-1');\n  titleElement.innerText = title;\n\n  const button = document.createElement('button');\n  button.setAttribute('type', 'button');\n  button.setAttribute('class', 'btn-close');\n  button.setAttribute('data-bs-dismiss', 'toast');\n  button.setAttribute('aria-label', 'Close');\n\n  const body = document.createElement('div');\n  body.setAttribute('class', 'toast-body');\n\n  header.appendChild(icon);\n  header.appendChild(titleElement);\n\n  if (typeof extra !== 'undefined') {\n    const extraElement = document.createElement('small');\n    extraElement.setAttribute('class', 'text-muted');\n    header.appendChild(extraElement);\n  }\n\n  header.appendChild(button);\n\n  body.innerText = message.trim();\n\n  main.appendChild(header);\n  main.appendChild(body);\n  container.appendChild(main);\n  document.body.appendChild(container);\n\n  const toast = new window.Toast(main);\n  return toast;\n}\n", "import Cookie from 'cookie';\n\ntype APIRes<T> = T | ErrorBase | APIError;\ntype Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\ntype ReqData = URLSearchParams | Dict | undefined | unknown;\n\n/**\n * Type guard to determine if an API response is a detailed error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function isApiError(data: Record<string, unknown>): data is APIError {\n  return 'error' in data && 'exception' in data;\n}\n\n/**\n * Type guard to determine if an API response is an error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function hasError(data: Record<string, unknown>): data is ErrorBase {\n  return 'error' in data;\n}\n\n/**\n * Type guard to determine if an element is an `HTMLInputElement`.\n *\n * @param element HTML Element.\n */\nexport function isInputElement(element: HTMLElement): element is HTMLInputElement {\n  return 'value' in element && 'required' in element;\n}\n\n/**\n * Retrieve the CSRF token from cookie storage.\n */\nexport function getCsrfToken(): string {\n  const { csrftoken: csrfToken } = Cookie.parse(document.cookie);\n  if (typeof csrfToken === 'undefined') {\n    throw new Error('Invalid or missing CSRF token');\n  }\n  return csrfToken;\n}\n\n/**\n * Authenticate and interact with the NetBox API.\n *\n * @param url Request URL\n * @param method Request Method\n * @param data Data to `POST`, `PATCH`, or `PUT`, if applicable.\n * @returns JSON Response\n */\nexport async function apiRequest<R extends Dict, D extends ReqData = undefined>(\n  url: string,\n  method: Method,\n  data?: D,\n): Promise<APIRes<R>> {\n  const token = getCsrfToken();\n  const headers = new Headers({ 'X-CSRFToken': token });\n\n  let body;\n  if (typeof data !== 'undefined') {\n    body = JSON.stringify(data);\n    headers.set('content-type', 'application/json');\n    headers.set('Accept', 'application/json');\n  }\n\n  const res = await fetch(url, { method, body, headers, credentials: 'same-origin' });\n  const contentType = res.headers.get('Content-Type');\n  if (typeof contentType === 'string' && contentType.includes('text')) {\n    const error = await res.text();\n    return { error } as ErrorBase;\n  }\n  const json = (await res.json()) as R | APIError;\n  if (!res.ok && Array.isArray(json)) {\n    const error = json.join('\\n');\n    return { error } as ErrorBase;\n  } else if (!res.ok && 'detail' in json) {\n    return { error: json.detail } as ErrorBase;\n  }\n  return json;\n}\n\n/**\n * `POST` an object as form data to the NetBox API.\n *\n * @param url Request URL\n * @param data Object to convert to form data\n * @returns JSON Response\n */\nexport async function apiPostForm<R extends Dict, D extends Dict>(\n  url: string,\n  data: D,\n): Promise<APIRes<R>> {\n  return await apiRequest<R, D>(url, 'POST', data);\n}\n\n/**\n * `GET` data from the NetBox API.\n *\n * @param url Request URL\n * @returns JSON Response\n */\nexport async function apiGetBase<R extends Dict>(url: string): Promise<APIRes<R>> {\n  return await apiRequest<R>(url, 'GET');\n}\n", "import { createToast } from './bs';\nimport { apiGetBase, apiPostForm, isApiError, isInputElement, hasError } from './util';\n\nimport type { APISecret, APIKeyPair } from './types';\n\n/**\n * Initialize Generate Private Key Pair Elements.\n */\nfunction initGenerateKeyPair() {\n  const element = document.getElementById('new_keypair_modal') as HTMLDivElement;\n  const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;\n  // If the elements are not loaded, stop.\n  if (element === null || accept === null) {\n    return;\n  }\n  const publicElem = element.querySelector<HTMLTextAreaElement>('textarea#new_pubkey');\n  const privateElem = element.querySelector<HTMLTextAreaElement>('textarea#new_privkey');\n\n  /**\n   * Handle Generate Private Key Pair Modal opening.\n   */\n  function handleOpen() {\n    // When the modal opens, set the `readonly` attribute on the textarea elements.\n    for (const elem of [publicElem, privateElem]) {\n      if (elem !== null) {\n        elem.setAttribute('readonly', '');\n      }\n    }\n    // Fetch the key pair from the API.\n    apiGetBase<APIKeyPair>('/api/plugins/secrets/generate-rsa-key-pair/').then(data => {\n      if (!hasError(data)) {\n        // If key pair generation was successful, set the textarea elements' value to the generated\n        // values.\n        const { private_key: priv, public_key: pub } = data;\n        if (publicElem !== null && privateElem !== null) {\n          publicElem.value = pub;\n          privateElem.value = priv;\n        }\n      } else {\n        // Otherwise, show an error.\n        const toast = createToast('danger', 'Error', data.error);\n        toast.show();\n      }\n    });\n  }\n\n  /**\n   * Set the public key form field's value to the generated public key.\n   */\n  function handleAccept() {\n    const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;\n    if (publicElem !== null) {\n      publicKeyField.value = publicElem.value;\n      publicKeyField.innerText = publicElem.value;\n    }\n  }\n  element.addEventListener('shown.bs.modal', () => handleOpen());\n  accept.addEventListener('click', () => handleAccept());\n}\n\n/**\n * Toggle copy/lock/unlock button visibility based on the action occurring.\n * @param id Secret ID.\n * @param action Lock or Unlock, so we know which buttons to display.\n */\nfunction toggleSecretButtons(id: string, action: 'lock' | 'unlock') {\n  const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`);\n  const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`);\n  const copyButton = document.querySelector(`button.copy-secret[secret-id='${id}']`);\n  // If we're unlocking, hide the unlock button. Otherwise, show it.\n  if (unlockButton !== null) {\n    if (action === 'unlock') unlockButton.classList.add('d-none');\n    if (action === 'lock') unlockButton.classList.remove('d-none');\n  }\n  // If we're unlocking, show the lock button. Otherwise, hide it.\n  if (lockButton !== null) {\n    if (action === 'unlock') lockButton.classList.remove('d-none');\n    if (action === 'lock') lockButton.classList.add('d-none');\n  }\n  // If we're unlocking, show the copy button. Otherwise, hide it.\n  if (copyButton !== null) {\n    if (action === 'unlock') copyButton.classList.remove('d-none');\n    if (action === 'lock') copyButton.classList.add('d-none');\n  }\n}\n\n/**\n * Initialize Lock & Unlock button event listeners & callbacks.\n */\nfunction initLockUnlock() {\n  const privateKeyModal = new window.Modal('#privkey_modal');\n\n  /**\n   * Unlock a secret, or prompt the user for their private key, if a session key is not available.\n   *\n   * @param id Secret ID\n   */\n  function unlock(id: string | null) {\n    const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n    if (typeof id === 'string' && id !== '') {\n      apiGetBase<APISecret>(`/api/plugins/secrets/secrets/${id}/`).then(data => {\n        if (!hasError(data)) {\n          const { plaintext } = data;\n          // `plaintext` is the plain text value of the secret. If it is null, it has not been\n          // decrypted, likely due to a mission session key.\n\n          if (target !== null && plaintext !== null) {\n            // If `plaintext` is not null, we have the decrypted value. Set the target element's\n            // inner text to the decrypted value and toggle copy/lock button visibility.\n            if (isInputElement(target)) {\n              target.value = plaintext;\n            } else {\n              target.innerText = plaintext;\n            }\n\n            toggleSecretButtons(id, 'unlock');\n          } else {\n            // Otherwise, we do _not_ have the decrypted value and need to prompt the user for\n            // their private RSA key, in order to get a session key. The session key is then sent\n            // as a cookie in future requests.\n            privateKeyModal.show();\n          }\n        } else {\n          if (data.error.toLowerCase().includes('invalid session key')) {\n            // If, for some reason, a request was made but resulted in an API error that complains\n            // of a missing session key, prompt the user for their session key.\n            privateKeyModal.show();\n          } else {\n            // If we received an API error but it doesn't contain 'invalid session key', show the\n            // user an error message.\n            const toast = createToast('danger', 'Error', data.error);\n            toast.show();\n          }\n        }\n      });\n    }\n  }\n\n  /**\n   * Lock a secret and toggle visibility of the unlock button.\n   * @param id Secret ID\n   */\n  function lock(id: string | null) {\n    if (typeof id === 'string' && id !== '') {\n      const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n\n      // Obscure the inner text of the secret element.\n      if (isInputElement(target)) {\n        target.value = '********';\n      } else {\n        target.innerText = '********';\n      }\n\n      // Toggle visibility of the copy/lock/unlock buttons.\n      toggleSecretButtons(id, 'lock');\n    }\n  }\n\n  for (const element of document.querySelectorAll<HTMLButtonElement>('button.unlock-secret')) {\n    element.addEventListener('click', () => unlock(element.getAttribute('secret-id')));\n  }\n  for (const element of document.querySelectorAll<HTMLButtonElement>('button.lock-secret')) {\n    element.addEventListener('click', () => lock(element.getAttribute('secret-id')));\n  }\n}\n\n/**\n * Request a session key from the API.\n * @param privateKey RSA Private Key (valid JSON string)\n */\nfunction requestSessionKey(privateKey: string) {\n  apiPostForm('/api/plugins/secrets/session-keys/', {\n    private_key: privateKey,\n    preserve_key: true,\n  }).then(res => {\n    if (!hasError(res)) {\n      // If the session key has been added from the user key page, reload the page.\n      if (window.location.pathname === '/plugins/secrets/user-key/') {\n        window.location.reload();\n      } else {\n        // If the response received was not an error, show the user a success message.\n        const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.');\n        toast.show();\n      }\n    } else {\n      // Otherwise, show the user an error message.\n      let message = res.error;\n      if (isApiError(res)) {\n        // If the error received was a standard API error containing a Python exception message,\n        // append it to the error.\n        message += `\\n${res.exception}`;\n      }\n      const toast = createToast('danger', 'Failed to Retrieve Session Key', message);\n      toast.show();\n    }\n  });\n}\n\n/**\n * Initialize Request Session Key Elements.\n */\nfunction initGetSessionKey() {\n  for (const element of document.querySelectorAll<HTMLButtonElement>('#request_session_key')) {\n    /**\n     * Send the user's input private key to the API to get a session key, which will be stored as\n     * a cookie for future requests.\n     */\n    function handleClick() {\n      for (const pk of document.querySelectorAll<HTMLTextAreaElement>('#user_privkey')) {\n        requestSessionKey(pk.value);\n        // Clear the private key form field value.\n        pk.value = '';\n      }\n    }\n    element.addEventListener('click', handleClick);\n  }\n}\n\n/**\n * Initialize Secret Edit Form Handler.\n */\nfunction initSecretsEdit() {\n  const privateKeyModal = new window.Modal('#privkey_modal');\n\n  /**\n   * Check the cookie store for a `netbox_secrets_sessionid`. If not present, prompt the user to submit their\n   * private key.\n   */\n  function handleSubmit(event: Event): void {\n    if (document.cookie.indexOf('netbox_secrets_sessionid') === -1) {\n      event.preventDefault();\n      privateKeyModal.show();\n    }\n  }\n\n  for (const element of document.querySelectorAll<HTMLInputElement>('.requires-session-key')) {\n    const form = element.closest<HTMLFormElement>('form');\n    if (form !== null) {\n      form.addEventListener('submit', handleSubmit);\n    }\n  }\n}\n\nexport function initSecrets() {\n  for (const func of [initGenerateKeyPair, initLockUnlock, initGetSessionKey, initSecretsEdit]) {\n    func();\n  }\n}\n", "import { initSecrets } from './secrets';\n\nif (document.readyState !== 'loading') {\n  initSecrets();\n} else {\n  document.addEventListener('DOMContentLoaded', initSecrets);\n}\n"],
-  "mappings": "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",
+  "sourcesContent": ["/*!\n * cookie\n * Copyright(c) 2012-2014 Roman Shtylman\n * Copyright(c) 2015 Douglas Christopher Wilson\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module exports.\n * @public\n */\n\nexports.parse = parse;\nexports.serialize = serialize;\n\n/**\n * Module variables.\n * @private\n */\n\nvar __toString = Object.prototype.toString\n\n/**\n * RegExp to match field-content in RFC 7230 sec 3.2\n *\n * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]\n * field-vchar   = VCHAR / obs-text\n * obs-text      = %x80-FF\n */\n\nvar fieldContentRegExp = /^[\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+$/;\n\n/**\n * Parse a cookie header.\n *\n * Parse the given cookie header string into an object\n * The object has the various cookies as keys(names) => values\n *\n * @param {string} str\n * @param {object} [options]\n * @return {object}\n * @public\n */\n\nfunction parse(str, options) {\n  if (typeof str !== 'string') {\n    throw new TypeError('argument str must be a string');\n  }\n\n  var obj = {}\n  var opt = options || {};\n  var dec = opt.decode || decode;\n\n  var index = 0\n  while (index < str.length) {\n    var eqIdx = str.indexOf('=', index)\n\n    // no more cookie pairs\n    if (eqIdx === -1) {\n      break\n    }\n\n    var endIdx = str.indexOf(';', index)\n\n    if (endIdx === -1) {\n      endIdx = str.length\n    } else if (endIdx < eqIdx) {\n      // backtrack on prior semicolon\n      index = str.lastIndexOf(';', eqIdx - 1) + 1\n      continue\n    }\n\n    var key = str.slice(index, eqIdx).trim()\n\n    // only assign once\n    if (undefined === obj[key]) {\n      var val = str.slice(eqIdx + 1, endIdx).trim()\n\n      // quoted values\n      if (val.charCodeAt(0) === 0x22) {\n        val = val.slice(1, -1)\n      }\n\n      obj[key] = tryDecode(val, dec);\n    }\n\n    index = endIdx + 1\n  }\n\n  return obj;\n}\n\n/**\n * Serialize data into a cookie header.\n *\n * Serialize the a name value pair into a cookie string suitable for\n * http headers. An optional options object specified cookie parameters.\n *\n * serialize('foo', 'bar', { httpOnly: true })\n *   => \"foo=bar; httpOnly\"\n *\n * @param {string} name\n * @param {string} val\n * @param {object} [options]\n * @return {string}\n * @public\n */\n\nfunction serialize(name, val, options) {\n  var opt = options || {};\n  var enc = opt.encode || encode;\n\n  if (typeof enc !== 'function') {\n    throw new TypeError('option encode is invalid');\n  }\n\n  if (!fieldContentRegExp.test(name)) {\n    throw new TypeError('argument name is invalid');\n  }\n\n  var value = enc(val);\n\n  if (value && !fieldContentRegExp.test(value)) {\n    throw new TypeError('argument val is invalid');\n  }\n\n  var str = name + '=' + value;\n\n  if (null != opt.maxAge) {\n    var maxAge = opt.maxAge - 0;\n\n    if (isNaN(maxAge) || !isFinite(maxAge)) {\n      throw new TypeError('option maxAge is invalid')\n    }\n\n    str += '; Max-Age=' + Math.floor(maxAge);\n  }\n\n  if (opt.domain) {\n    if (!fieldContentRegExp.test(opt.domain)) {\n      throw new TypeError('option domain is invalid');\n    }\n\n    str += '; Domain=' + opt.domain;\n  }\n\n  if (opt.path) {\n    if (!fieldContentRegExp.test(opt.path)) {\n      throw new TypeError('option path is invalid');\n    }\n\n    str += '; Path=' + opt.path;\n  }\n\n  if (opt.expires) {\n    var expires = opt.expires\n\n    if (!isDate(expires) || isNaN(expires.valueOf())) {\n      throw new TypeError('option expires is invalid');\n    }\n\n    str += '; Expires=' + expires.toUTCString()\n  }\n\n  if (opt.httpOnly) {\n    str += '; HttpOnly';\n  }\n\n  if (opt.secure) {\n    str += '; Secure';\n  }\n\n  if (opt.priority) {\n    var priority = typeof opt.priority === 'string'\n      ? opt.priority.toLowerCase()\n      : opt.priority\n\n    switch (priority) {\n      case 'low':\n        str += '; Priority=Low'\n        break\n      case 'medium':\n        str += '; Priority=Medium'\n        break\n      case 'high':\n        str += '; Priority=High'\n        break\n      default:\n        throw new TypeError('option priority is invalid')\n    }\n  }\n\n  if (opt.sameSite) {\n    var sameSite = typeof opt.sameSite === 'string'\n      ? opt.sameSite.toLowerCase() : opt.sameSite;\n\n    switch (sameSite) {\n      case true:\n        str += '; SameSite=Strict';\n        break;\n      case 'lax':\n        str += '; SameSite=Lax';\n        break;\n      case 'strict':\n        str += '; SameSite=Strict';\n        break;\n      case 'none':\n        str += '; SameSite=None';\n        break;\n      default:\n        throw new TypeError('option sameSite is invalid');\n    }\n  }\n\n  return str;\n}\n\n/**\n * URL-decode string value. Optimized to skip native call when no %.\n *\n * @param {string} str\n * @returns {string}\n */\n\nfunction decode (str) {\n  return str.indexOf('%') !== -1\n    ? decodeURIComponent(str)\n    : str\n}\n\n/**\n * URL-encode value.\n *\n * @param {string} str\n * @returns {string}\n */\n\nfunction encode (val) {\n  return encodeURIComponent(val)\n}\n\n/**\n * Determine if value is a Date.\n *\n * @param {*} val\n * @private\n */\n\nfunction isDate (val) {\n  return __toString.call(val) === '[object Date]' ||\n    val instanceof Date\n}\n\n/**\n * Try decoding a string using a decoding function.\n *\n * @param {string} str\n * @param {function} decode\n * @private\n */\n\nfunction tryDecode(str, decode) {\n  try {\n    return decode(str);\n  } catch (e) {\n    return str;\n  }\n}\n", "type ToastLevel = 'danger' | 'warning' | 'success' | 'info';\n\nexport function createToast(\n  level: ToastLevel,\n  title: string,\n  message: string,\n  extra?: string,\n): InstanceType<typeof window.Toast> {\n  let iconName = 'mdi-alert';\n  switch (level) {\n    case 'warning':\n      iconName = 'mdi-alert';\n      break;\n    case 'success':\n      iconName = 'mdi-check-circle';\n      break;\n    case 'info':\n      iconName = 'mdi-information';\n      break;\n    case 'danger':\n      iconName = 'mdi-alert';\n      break;\n  }\n\n  const container = document.createElement('div');\n  container.setAttribute('class', 'toast-container position-fixed bottom-0 end-0 m-3');\n\n  const main = document.createElement('div');\n  main.setAttribute('class', `toast bg-${level}`);\n  main.setAttribute('role', 'alert');\n  main.setAttribute('aria-live', 'assertive');\n  main.setAttribute('aria-atomic', 'true');\n\n  const header = document.createElement('div');\n  header.setAttribute('class', `toast-header bg-${level} text-body`);\n\n  const icon = document.createElement('i');\n  icon.setAttribute('class', `mdi ${iconName}`);\n\n  const titleElement = document.createElement('strong');\n  titleElement.setAttribute('class', 'me-auto ms-1');\n  titleElement.innerText = title;\n\n  const button = document.createElement('button');\n  button.setAttribute('type', 'button');\n  button.setAttribute('class', 'btn-close');\n  button.setAttribute('data-bs-dismiss', 'toast');\n  button.setAttribute('aria-label', 'Close');\n\n  const body = document.createElement('div');\n  body.setAttribute('class', 'toast-body');\n\n  header.appendChild(icon);\n  header.appendChild(titleElement);\n\n  if (typeof extra !== 'undefined') {\n    const extraElement = document.createElement('small');\n    extraElement.setAttribute('class', 'text-muted');\n    header.appendChild(extraElement);\n  }\n\n  header.appendChild(button);\n\n  body.innerText = message.trim();\n\n  main.appendChild(header);\n  main.appendChild(body);\n  container.appendChild(main);\n  document.body.appendChild(container);\n\n  const toast = new window.Toast(main);\n  return toast;\n}\n", "import Cookie from 'cookie';\n\ntype APIRes<T> = T | ErrorBase | APIError;\ntype Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';\ntype ReqData = URLSearchParams | Dict | undefined | unknown;\n\n/**\n * Type guard to determine if an API response is a detailed error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function isApiError(data: Record<string, unknown>): data is APIError {\n  return 'error' in data && 'exception' in data;\n}\n\n/**\n * Type guard to determine if an API response is an error.\n *\n * @param data API JSON Response\n * @returns Type guard for `data`.\n */\nexport function hasError(data: Record<string, unknown>): data is ErrorBase {\n  return 'error' in data;\n}\n\n/**\n * Type guard to determine if an element is an `HTMLInputElement`.\n *\n * @param element HTML Element.\n */\nexport function isInputElement(element: HTMLElement): element is HTMLInputElement {\n  return 'value' in element && 'required' in element;\n}\n\n/**\n * Retrieve the CSRF token from cookie storage.\n */\nexport function getCsrfToken(): string {\n  const { csrftoken: csrfToken } = Cookie.parse(document.cookie);\n  if (typeof csrfToken === 'undefined') {\n    throw new Error('Invalid or missing CSRF token');\n  }\n  return csrfToken;\n}\n\n/**\n * Authenticate and interact with the NetBox API.\n *\n * @param url Request URL\n * @param method Request Method\n * @param data Data to `POST`, `PATCH`, or `PUT`, if applicable.\n * @returns JSON Response\n */\nexport async function apiRequest<R extends Dict, D extends ReqData = undefined>(\n  url: string,\n  method: Method,\n  data?: D,\n): Promise<APIRes<R>> {\n  const token = getCsrfToken();\n  const headers = new Headers({ 'X-CSRFToken': token });\n\n  let body;\n  if (typeof data !== 'undefined') {\n    body = JSON.stringify(data);\n    headers.set('content-type', 'application/json');\n    headers.set('Accept', 'application/json');\n  }\n\n  const res = await fetch(url, { method, body, headers, credentials: 'same-origin' });\n  const contentType = res.headers.get('Content-Type');\n  if (typeof contentType === 'string' && contentType.includes('text')) {\n    const error = await res.text();\n    return { error } as ErrorBase;\n  }\n  const json = (await res.json()) as R | APIError;\n  if (!res.ok && Array.isArray(json)) {\n    const error = json.join('\\n');\n    return { error } as ErrorBase;\n  } else if (!res.ok && 'detail' in json) {\n    return { error: json.detail } as ErrorBase;\n  }\n  return json;\n}\n\n/**\n * `POST` an object as form data to the NetBox API.\n *\n * @param url Request URL\n * @param data Object to convert to form data\n * @returns JSON Response\n */\nexport async function apiPostForm<R extends Dict, D extends Dict>(\n  url: string,\n  data: D,\n): Promise<APIRes<R>> {\n  return await apiRequest<R, D>(url, 'POST', data);\n}\n\n/**\n * `GET` data from the NetBox API.\n *\n * @param url Request URL\n * @returns JSON Response\n */\nexport async function apiGetBase<R extends Dict>(url: string): Promise<APIRes<R>> {\n  return await apiRequest<R>(url, 'GET');\n}\n", "import { createToast } from './bs';\nimport { apiGetBase, apiPostForm, isApiError, isInputElement, hasError } from './util';\n\nimport type { APISecret, APIKeyPair } from './types';\n\n/**\n * Initialize Generate Private Key Pair Elements.\n */\nfunction initGenerateKeyPair() {\n  const element = document.getElementById('new_keypair_modal') as HTMLDivElement;\n  const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;\n  // If the elements are not loaded, stop.\n  if (element === null || accept === null) {\n    return;\n  }\n  const publicElem = element.querySelector<HTMLTextAreaElement>('textarea#new_pubkey');\n  const privateElem = element.querySelector<HTMLTextAreaElement>('textarea#new_privkey');\n\n  /**\n   * Handle Generate Private Key Pair Modal opening.\n   */\n  function handleOpen() {\n    // When the modal opens, set the `readonly` attribute on the textarea elements.\n    for (const elem of [publicElem, privateElem]) {\n      if (elem !== null) {\n        elem.setAttribute('readonly', '');\n      }\n    }\n    // Fetch the key pair from the API.\n    apiGetBase<APIKeyPair>('/api/plugins/secrets/generate-rsa-key-pair/').then(data => {\n      if (!hasError(data)) {\n        // If key pair generation was successful, set the textarea elements' value to the generated\n        // values.\n        const { private_key: priv, public_key: pub } = data;\n        if (publicElem !== null && privateElem !== null) {\n          publicElem.value = pub;\n          privateElem.value = priv;\n        }\n      } else {\n        // Otherwise, show an error.\n        const toast = createToast('danger', 'Error', data.error);\n        toast.show();\n      }\n    });\n  }\n\n  /**\n   * Set the public key form field's value to the generated public key.\n   */\n  function handleAccept() {\n    const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;\n    if (publicElem !== null) {\n      publicKeyField.value = publicElem.value;\n      publicKeyField.innerText = publicElem.value;\n    }\n  }\n  element.addEventListener('shown.bs.modal', () => handleOpen());\n  accept.addEventListener('click', () => handleAccept());\n}\n\n/**\n * Toggle copy/lock/unlock button visibility based on the action occurring.\n * @param id Secret ID.\n * @param action Lock or Unlock, so we know which buttons to display.\n */\nfunction toggleSecretButtons(id: string, action: 'lock' | 'unlock') {\n  const unlockButton = document.querySelector(`button.unlock-secret[secret-id='${id}']`);\n  const lockButton = document.querySelector(`button.lock-secret[secret-id='${id}']`);\n  const copyButton = document.querySelector(`span[secret-id='${id}']`);\n  console.log(copyButton);\n  // If we're unlocking, hide the unlock button. Otherwise, show it.\n  if (unlockButton !== null) {\n    if (action === 'unlock') unlockButton.classList.add('d-none');\n    if (action === 'lock') unlockButton.classList.remove('d-none');\n  }\n  // If we're unlocking, show the lock button. Otherwise, hide it.\n  if (lockButton !== null) {\n    if (action === 'unlock') lockButton.classList.remove('d-none');\n    if (action === 'lock') lockButton.classList.add('d-none');\n  }\n  // If we're unlocking, show the copy button. Otherwise, hide it.\n  if (copyButton !== null) {\n    if (action === 'unlock') copyButton.classList.remove('d-none');\n    if (action === 'lock') copyButton.classList.add('d-none');\n  }\n}\n\n/**\n * Initialize Lock & Unlock button event listeners & callbacks.\n */\nfunction initLockUnlock() {\n  const privateKeyModal = new window.Modal('#privkey_modal');\n\n  /**\n   * Unlock a secret, or prompt the user for their private key, if a session key is not available.\n   *\n   * @param id Secret ID\n   */\n  function unlock(id: string | null) {\n    const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n    if (typeof id === 'string' && id !== '') {\n      apiGetBase<APISecret>(`/api/plugins/secrets/secrets/${id}/`).then(data => {\n        if (!hasError(data)) {\n          const { plaintext } = data;\n          // `plaintext` is the plain text value of the secret. If it is null, it has not been\n          // decrypted, likely due to a mission session key.\n\n          if (target !== null && plaintext !== null) {\n            // If `plaintext` is not null, we have the decrypted value. Set the target element's\n            // inner text to the decrypted value and toggle copy/lock button visibility.\n            if (isInputElement(target)) {\n              target.value = plaintext;\n            } else {\n              target.innerText = plaintext;\n            }\n\n            toggleSecretButtons(id, 'unlock');\n          } else {\n            // Otherwise, we do _not_ have the decrypted value and need to prompt the user for\n            // their private RSA key, in order to get a session key. The session key is then sent\n            // as a cookie in future requests.\n            privateKeyModal.show();\n          }\n        } else {\n          if (data.error.toLowerCase().includes('invalid session key')) {\n            // If, for some reason, a request was made but resulted in an API error that complains\n            // of a missing session key, prompt the user for their session key.\n            privateKeyModal.show();\n          } else {\n            // If we received an API error but it doesn't contain 'invalid session key', show the\n            // user an error message.\n            const toast = createToast('danger', 'Error', data.error);\n            toast.show();\n          }\n        }\n      });\n    }\n  }\n\n  /**\n   * Lock a secret and toggle visibility of the unlock button.\n   * @param id Secret ID\n   */\n  function lock(id: string | null) {\n    if (typeof id === 'string' && id !== '') {\n      const target = document.getElementById(`secret_${id}`) as HTMLDivElement | HTMLInputElement;\n\n      // Obscure the inner text of the secret element.\n      if (isInputElement(target)) {\n        target.value = '********';\n      } else {\n        target.innerText = '********';\n      }\n\n      // Toggle visibility of the copy/lock/unlock buttons.\n      toggleSecretButtons(id, 'lock');\n    }\n  }\n\n  for (const element of document.querySelectorAll<HTMLButtonElement>('button.unlock-secret')) {\n    element.addEventListener('click', () => unlock(element.getAttribute('secret-id')));\n  }\n  for (const element of document.querySelectorAll<HTMLButtonElement>('button.lock-secret')) {\n    element.addEventListener('click', () => lock(element.getAttribute('secret-id')));\n  }\n}\n\n/**\n * Request a session key from the API.\n * @param privateKey RSA Private Key (valid JSON string)\n */\nfunction requestSessionKey(privateKey: string) {\n  apiPostForm('/api/plugins/secrets/session-keys/', {\n    private_key: privateKey,\n    preserve_key: true,\n  }).then(res => {\n    if (!hasError(res)) {\n      // If the session key has been added from the user key page, reload the page.\n      if (window.location.pathname === '/plugins/secrets/user-key/') {\n        window.location.reload();\n      } else {\n        // If the response received was not an error, show the user a success message.\n        const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.');\n        toast.show();\n      }\n    } else {\n      // Otherwise, show the user an error message.\n      let message = res.error;\n      if (isApiError(res)) {\n        // If the error received was a standard API error containing a Python exception message,\n        // append it to the error.\n        message += `\\n${res.exception}`;\n      }\n      const toast = createToast('danger', 'Failed to Retrieve Session Key', message);\n      toast.show();\n    }\n  });\n}\n\n/**\n * Initialize Request Session Key Elements.\n */\nfunction initGetSessionKey() {\n  for (const element of document.querySelectorAll<HTMLButtonElement>('#request_session_key')) {\n    /**\n     * Send the user's input private key to the API to get a session key, which will be stored as\n     * a cookie for future requests.\n     */\n    function handleClick() {\n      for (const pk of document.querySelectorAll<HTMLTextAreaElement>('#user_privkey')) {\n        requestSessionKey(pk.value);\n        // Clear the private key form field value.\n        pk.value = '';\n      }\n    }\n    element.addEventListener('click', handleClick);\n  }\n}\n\n/**\n * Initialize Secret Edit Form Handler.\n */\nfunction initSecretsEdit() {\n  const privateKeyModal = new window.Modal('#privkey_modal');\n\n  /**\n   * Check the cookie store for a `netbox_secrets_sessionid`. If not present, prompt the user to submit their\n   * private key.\n   */\n  function handleSubmit(event: Event): void {\n    if (document.cookie.indexOf('netbox_secrets_sessionid') === -1) {\n      event.preventDefault();\n      privateKeyModal.show();\n    }\n  }\n\n  for (const element of document.querySelectorAll<HTMLInputElement>('.requires-session-key')) {\n    const form = element.closest<HTMLFormElement>('form');\n    if (form !== null) {\n      form.addEventListener('submit', handleSubmit);\n    }\n  }\n}\n\nexport function initSecrets() {\n  for (const func of [initGenerateKeyPair, initLockUnlock, initGetSessionKey, initSecretsEdit]) {\n    func();\n  }\n}\n", "import { initSecrets } from './secrets';\n\nif (document.readyState !== 'loading') {\n  initSecrets();\n} else {\n  document.addEventListener('DOMContentLoaded', initSecrets);\n}\n"],
+  "mappings": "yuBAAA,IAAAA,EAAAC,EAAAC,GAAA,cAcAA,EAAQ,MAAQC,EAChBD,EAAQ,UAAYE,EAOpB,IAAIC,EAAa,OAAO,UAAU,SAU9BC,EAAqB,wCAczB,SAASH,EAAMI,EAAKC,EAAS,CAC3B,GAAI,OAAOD,GAAQ,SACjB,MAAM,IAAI,UAAU,+BAA+B,EAQrD,QALIE,EAAM,CAAC,EACPC,EAAMF,GAAW,CAAC,EAClBG,EAAMD,EAAI,QAAUE,EAEpBC,EAAQ,EACLA,EAAQN,EAAI,QAAQ,CACzB,IAAIO,EAAQP,EAAI,QAAQ,IAAKM,CAAK,EAGlC,GAAIC,IAAU,GACZ,MAGF,IAAIC,EAASR,EAAI,QAAQ,IAAKM,CAAK,EAEnC,GAAIE,IAAW,GACbA,EAASR,EAAI,eACJQ,EAASD,EAAO,CAEzBD,EAAQN,EAAI,YAAY,IAAKO,EAAQ,CAAC,EAAI,EAC1C,SAGF,IAAIE,EAAMT,EAAI,MAAMM,EAAOC,CAAK,EAAE,KAAK,EAGvC,GAAkBL,EAAIO,CAAG,IAArB,OAAwB,CAC1B,IAAIC,EAAMV,EAAI,MAAMO,EAAQ,EAAGC,CAAM,EAAE,KAAK,EAGxCE,EAAI,WAAW,CAAC,IAAM,KACxBA,EAAMA,EAAI,MAAM,EAAG,EAAE,GAGvBR,EAAIO,CAAG,EAAIE,EAAUD,EAAKN,CAAG,EAG/BE,EAAQE,EAAS,EAGnB,OAAON,CACT,CAkBA,SAASL,EAAUe,EAAMF,EAAKT,EAAS,CACrC,IAAIE,EAAMF,GAAW,CAAC,EAClBY,EAAMV,EAAI,QAAUW,EAExB,GAAI,OAAOD,GAAQ,WACjB,MAAM,IAAI,UAAU,0BAA0B,EAGhD,GAAI,CAACd,EAAmB,KAAKa,CAAI,EAC/B,MAAM,IAAI,UAAU,0BAA0B,EAGhD,IAAIG,EAAQF,EAAIH,CAAG,EAEnB,GAAIK,GAAS,CAAChB,EAAmB,KAAKgB,CAAK,EACzC,MAAM,IAAI,UAAU,yBAAyB,EAG/C,IAAIf,EAAMY,EAAO,IAAMG,EAEvB,GAAYZ,EAAI,QAAZ,KAAoB,CACtB,IAAIa,EAASb,EAAI,OAAS,EAE1B,GAAI,MAAMa,CAAM,GAAK,CAAC,SAASA,CAAM,EACnC,MAAM,IAAI,UAAU,0BAA0B,EAGhDhB,GAAO,aAAe,KAAK,MAAMgB,CAAM,EAGzC,GAAIb,EAAI,OAAQ,CACd,GAAI,CAACJ,EAAmB,KAAKI,EAAI,MAAM,EACrC,MAAM,IAAI,UAAU,0BAA0B,EAGhDH,GAAO,YAAcG,EAAI,OAG3B,GAAIA,EAAI,KAAM,CACZ,GAAI,CAACJ,EAAmB,KAAKI,EAAI,IAAI,EACnC,MAAM,IAAI,UAAU,wBAAwB,EAG9CH,GAAO,UAAYG,EAAI,KAGzB,GAAIA,EAAI,QAAS,CACf,IAAIc,EAAUd,EAAI,QAElB,GAAI,CAACe,EAAOD,CAAO,GAAK,MAAMA,EAAQ,QAAQ,CAAC,EAC7C,MAAM,IAAI,UAAU,2BAA2B,EAGjDjB,GAAO,aAAeiB,EAAQ,YAAY,EAW5C,GARId,EAAI,WACNH,GAAO,cAGLG,EAAI,SACNH,GAAO,YAGLG,EAAI,SAAU,CAChB,IAAIgB,EAAW,OAAOhB,EAAI,UAAa,SACnCA,EAAI,SAAS,YAAY,EACzBA,EAAI,SAER,OAAQgB,EAAU,CAChB,IAAK,MACHnB,GAAO,iBACP,MACF,IAAK,SACHA,GAAO,oBACP,MACF,IAAK,OACHA,GAAO,kBACP,MACF,QACE,MAAM,IAAI,UAAU,4BAA4B,CACpD,EAGF,GAAIG,EAAI,SAAU,CAChB,IAAIiB,EAAW,OAAOjB,EAAI,UAAa,SACnCA,EAAI,SAAS,YAAY,EAAIA,EAAI,SAErC,OAAQiB,EAAU,CAChB,IAAK,GACHpB,GAAO,oBACP,MACF,IAAK,MACHA,GAAO,iBACP,MACF,IAAK,SACHA,GAAO,oBACP,MACF,IAAK,OACHA,GAAO,kBACP,MACF,QACE,MAAM,IAAI,UAAU,4BAA4B,CACpD,EAGF,OAAOA,CACT,CASA,SAASK,EAAQL,EAAK,CACpB,OAAOA,EAAI,QAAQ,GAAG,IAAM,GACxB,mBAAmBA,CAAG,EACtBA,CACN,CASA,SAASc,EAAQJ,EAAK,CACpB,OAAO,mBAAmBA,CAAG,CAC/B,CASA,SAASQ,EAAQR,EAAK,CACpB,OAAOZ,EAAW,KAAKY,CAAG,IAAM,iBAC9BA,aAAe,IACnB,CAUA,SAASC,EAAUX,EAAKK,EAAQ,CAC9B,GAAI,CACF,OAAOA,EAAOL,CAAG,CACnB,OAASqB,EAAP,CACA,OAAOrB,CACT,CACF,IC3QO,SAASsB,EACdC,EACAC,EACAC,EACAC,EACmC,CACnC,IAAIC,EAAW,YACf,OAAQJ,EAAO,CACb,IAAK,UACHI,EAAW,YACX,MACF,IAAK,UACHA,EAAW,mBACX,MACF,IAAK,OACHA,EAAW,kBACX,MACF,IAAK,SACHA,EAAW,YACX,KACJ,CAEA,IAAMC,EAAY,SAAS,cAAc,KAAK,EAC9CA,EAAU,aAAa,QAAS,mDAAmD,EAEnF,IAAMC,EAAO,SAAS,cAAc,KAAK,EACzCA,EAAK,aAAa,QAAS,YAAYN,GAAO,EAC9CM,EAAK,aAAa,OAAQ,OAAO,EACjCA,EAAK,aAAa,YAAa,WAAW,EAC1CA,EAAK,aAAa,cAAe,MAAM,EAEvC,IAAMC,EAAS,SAAS,cAAc,KAAK,EAC3CA,EAAO,aAAa,QAAS,mBAAmBP,aAAiB,EAEjE,IAAMQ,EAAO,SAAS,cAAc,GAAG,EACvCA,EAAK,aAAa,QAAS,OAAOJ,GAAU,EAE5C,IAAMK,EAAe,SAAS,cAAc,QAAQ,EACpDA,EAAa,aAAa,QAAS,cAAc,EACjDA,EAAa,UAAYR,EAEzB,IAAMS,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,aAAa,OAAQ,QAAQ,EACpCA,EAAO,aAAa,QAAS,WAAW,EACxCA,EAAO,aAAa,kBAAmB,OAAO,EAC9CA,EAAO,aAAa,aAAc,OAAO,EAEzC,IAAMC,EAAO,SAAS,cAAc,KAAK,EAMzC,GALAA,EAAK,aAAa,QAAS,YAAY,EAEvCJ,EAAO,YAAYC,CAAI,EACvBD,EAAO,YAAYE,CAAY,EAE3B,OAAON,GAAU,YAAa,CAChC,IAAMS,EAAe,SAAS,cAAc,OAAO,EACnDA,EAAa,aAAa,QAAS,YAAY,EAC/CL,EAAO,YAAYK,CAAY,EAGjC,OAAAL,EAAO,YAAYG,CAAM,EAEzBC,EAAK,UAAYT,EAAQ,KAAK,EAE9BI,EAAK,YAAYC,CAAM,EACvBD,EAAK,YAAYK,CAAI,EACrBN,EAAU,YAAYC,CAAI,EAC1B,SAAS,KAAK,YAAYD,CAAS,EAErB,IAAI,OAAO,MAAMC,CAAI,CAErC,CCxEA,IAAAO,EAAmB,OAYZ,SAASC,EAAWC,EAAiD,CAC1E,MAAO,UAAWA,GAAQ,cAAeA,CAC3C,CAQO,SAASC,EAASD,EAAkD,CACzE,MAAO,UAAWA,CACpB,CAOO,SAASE,EAAeC,EAAmD,CAChF,MAAO,UAAWA,GAAW,aAAcA,CAC7C,CAKO,SAASC,GAAuB,CACrC,GAAM,CAAE,UAAWC,CAAU,EAAI,EAAAC,QAAO,MAAM,SAAS,MAAM,EAC7D,GAAI,OAAOD,GAAc,YACvB,MAAM,IAAI,MAAM,+BAA+B,EAEjD,OAAOA,CACT,CAUA,SAAsBE,EACpBC,EACAC,EACAT,EACoB,QAAAU,EAAA,sBACpB,IAAMC,EAAQP,EAAa,EACrBQ,EAAU,IAAI,QAAQ,CAAE,cAAeD,CAAM,CAAC,EAEhDE,EACA,OAAOb,GAAS,cAClBa,EAAO,KAAK,UAAUb,CAAI,EAC1BY,EAAQ,IAAI,eAAgB,kBAAkB,EAC9CA,EAAQ,IAAI,SAAU,kBAAkB,GAG1C,IAAME,EAAM,MAAM,MAAMN,EAAK,CAAE,OAAAC,EAAQ,KAAAI,EAAM,QAAAD,EAAS,YAAa,aAAc,CAAC,EAC5EG,EAAcD,EAAI,QAAQ,IAAI,cAAc,EAClD,GAAI,OAAOC,GAAgB,UAAYA,EAAY,SAAS,MAAM,EAEhE,MAAO,CAAE,MADK,MAAMD,EAAI,KAAK,CACd,EAEjB,IAAME,EAAQ,MAAMF,EAAI,KAAK,EAC7B,MAAI,CAACA,EAAI,IAAM,MAAM,QAAQE,CAAI,EAExB,CAAE,MADKA,EAAK,KAAK;AAAA,CAAI,CACb,EACN,CAACF,EAAI,IAAM,WAAYE,EACzB,CAAE,MAAOA,EAAK,MAAO,EAEvBA,CACT,GASA,SAAsBC,EACpBT,EACAR,EACoB,QAAAU,EAAA,sBACpB,OAAO,MAAMH,EAAiBC,EAAK,OAAQR,CAAI,CACjD,GAQA,SAAsBkB,EAA2BV,EAAiC,QAAAE,EAAA,sBAChF,OAAO,MAAMH,EAAcC,EAAK,KAAK,CACvC,GCnGA,SAASW,GAAsB,CAC7B,IAAMC,EAAU,SAAS,eAAe,mBAAmB,EACrDC,EAAS,SAAS,eAAe,gBAAgB,EAEvD,GAAID,IAAY,MAAQC,IAAW,KACjC,OAEF,IAAMC,EAAaF,EAAQ,cAAmC,qBAAqB,EAC7EG,EAAcH,EAAQ,cAAmC,sBAAsB,EAKrF,SAASI,GAAa,CAEpB,QAAWC,IAAQ,CAACH,EAAYC,CAAW,EACrCE,IAAS,MACXA,EAAK,aAAa,WAAY,EAAE,EAIpCC,EAAuB,6CAA6C,EAAE,KAAKC,GAAQ,CACjF,GAAKC,EAASD,CAAI,EAUFE,EAAY,SAAU,QAASF,EAAK,KAAK,EACjD,KAAK,MAXQ,CAGnB,GAAM,CAAE,YAAaG,EAAM,WAAYC,CAAI,EAAIJ,EAC3CL,IAAe,MAAQC,IAAgB,OACzCD,EAAW,MAAQS,EACnBR,EAAY,MAAQO,GAO1B,CAAC,CACH,CAKA,SAASE,GAAe,CACtB,IAAMC,EAAiB,SAAS,eAAe,eAAe,EAC1DX,IAAe,OACjBW,EAAe,MAAQX,EAAW,MAClCW,EAAe,UAAYX,EAAW,MAE1C,CACAF,EAAQ,iBAAiB,iBAAkB,IAAMI,EAAW,CAAC,EAC7DH,EAAO,iBAAiB,QAAS,IAAMW,EAAa,CAAC,CACvD,CAOA,SAASE,EAAoBC,EAAYC,EAA2B,CAClE,IAAMC,EAAe,SAAS,cAAc,mCAAmCF,KAAM,EAC/EG,EAAa,SAAS,cAAc,iCAAiCH,KAAM,EAC3EI,EAAa,SAAS,cAAc,mBAAmBJ,KAAM,EACnE,QAAQ,IAAII,CAAU,EAElBF,IAAiB,OACfD,IAAW,UAAUC,EAAa,UAAU,IAAI,QAAQ,EACxDD,IAAW,QAAQC,EAAa,UAAU,OAAO,QAAQ,GAG3DC,IAAe,OACbF,IAAW,UAAUE,EAAW,UAAU,OAAO,QAAQ,EACzDF,IAAW,QAAQE,EAAW,UAAU,IAAI,QAAQ,GAGtDC,IAAe,OACbH,IAAW,UAAUG,EAAW,UAAU,OAAO,QAAQ,EACzDH,IAAW,QAAQG,EAAW,UAAU,IAAI,QAAQ,EAE5D,CAKA,SAASC,GAAiB,CACxB,IAAMC,EAAkB,IAAI,OAAO,MAAM,gBAAgB,EAOzD,SAASC,EAAOP,EAAmB,CACjC,IAAMQ,EAAS,SAAS,eAAe,UAAUR,GAAI,EACjD,OAAOA,GAAO,UAAYA,IAAO,IACnCT,EAAsB,gCAAgCS,IAAK,EAAE,KAAKR,GAAQ,CACxE,GAAKC,EAASD,CAAI,EAsBZA,EAAK,MAAM,YAAY,EAAE,SAAS,qBAAqB,EAGzDc,EAAgB,KAAK,EAIPZ,EAAY,SAAU,QAASF,EAAK,KAAK,EACjD,KAAK,MA9BM,CACnB,GAAM,CAAE,UAAAiB,CAAU,EAAIjB,EAIlBgB,IAAW,MAAQC,IAAc,MAG/BC,EAAeF,CAAM,EACvBA,EAAO,MAAQC,EAEfD,EAAO,UAAYC,EAGrBV,EAAoBC,EAAI,QAAQ,GAKhCM,EAAgB,KAAK,EAc3B,CAAC,CAEL,CAMA,SAASK,EAAKX,EAAmB,CAC/B,GAAI,OAAOA,GAAO,UAAYA,IAAO,GAAI,CACvC,IAAMQ,EAAS,SAAS,eAAe,UAAUR,GAAI,EAGjDU,EAAeF,CAAM,EACvBA,EAAO,MAAQ,WAEfA,EAAO,UAAY,WAIrBT,EAAoBC,EAAI,MAAM,EAElC,CAEA,QAAWf,KAAW,SAAS,iBAAoC,sBAAsB,EACvFA,EAAQ,iBAAiB,QAAS,IAAMsB,EAAOtB,EAAQ,aAAa,WAAW,CAAC,CAAC,EAEnF,QAAWA,KAAW,SAAS,iBAAoC,oBAAoB,EACrFA,EAAQ,iBAAiB,QAAS,IAAM0B,EAAK1B,EAAQ,aAAa,WAAW,CAAC,CAAC,CAEnF,CAMA,SAAS2B,EAAkBC,EAAoB,CAC7CC,EAAY,qCAAsC,CAChD,YAAaD,EACb,aAAc,EAChB,CAAC,EAAE,KAAKE,GAAO,CACb,GAAI,CAACtB,EAASsB,CAAG,EAEX,OAAO,SAAS,WAAa,6BAC/B,OAAO,SAAS,OAAO,EAGTrB,EAAY,UAAW,uBAAwB,6BAA6B,EACpF,KAAK,MAER,CAEL,IAAIsB,EAAUD,EAAI,MACdE,EAAWF,CAAG,IAGhBC,GAAW;AAAA,EAAKD,EAAI,aAERrB,EAAY,SAAU,iCAAkCsB,CAAO,EACvE,KAAK,EAEf,CAAC,CACH,CAKA,SAASE,GAAoB,CAC3B,QAAWjC,KAAW,SAAS,iBAAoC,sBAAsB,EAAG,CAK1F,IAASkC,EAAT,UAAuB,CACrB,QAAWC,KAAM,SAAS,iBAAsC,eAAe,EAC7ER,EAAkBQ,EAAG,KAAK,EAE1BA,EAAG,MAAQ,EAEf,EANS,IAAAD,IAOTlC,EAAQ,iBAAiB,QAASkC,CAAW,EAEjD,CAKA,SAASE,GAAkB,CACzB,IAAMf,EAAkB,IAAI,OAAO,MAAM,gBAAgB,EAMzD,SAASgB,EAAaC,EAAoB,CACpC,SAAS,OAAO,QAAQ,0BAA0B,IAAM,KAC1DA,EAAM,eAAe,EACrBjB,EAAgB,KAAK,EAEzB,CAEA,QAAWrB,KAAW,SAAS,iBAAmC,uBAAuB,EAAG,CAC1F,IAAMuC,EAAOvC,EAAQ,QAAyB,MAAM,EAChDuC,IAAS,MACXA,EAAK,iBAAiB,SAAUF,CAAY,EAGlD,CAEO,SAASG,GAAc,CAC5B,QAAWC,IAAQ,CAAC1C,EAAqBqB,EAAgBa,EAAmBG,CAAe,EACzFK,EAAK,CAET,CCtPI,SAAS,aAAe,UAC1BC,EAAY,EAEZ,SAAS,iBAAiB,mBAAoBA,CAAW",
   "names": ["require_cookie", "__commonJSMin", "exports", "parse", "serialize", "__toString", "fieldContentRegExp", "str", "options", "obj", "opt", "dec", "decode", "index", "eqIdx", "endIdx", "key", "val", "tryDecode", "name", "enc", "encode", "value", "maxAge", "expires", "isDate", "priority", "sameSite", "e", "createToast", "level", "title", "message", "extra", "iconName", "container", "main", "header", "icon", "titleElement", "button", "body", "extraElement", "import_cookie", "isApiError", "data", "hasError", "isInputElement", "element", "getCsrfToken", "csrfToken", "Cookie", "apiRequest", "url", "method", "__async", "token", "headers", "body", "res", "contentType", "json", "apiPostForm", "apiGetBase", "initGenerateKeyPair", "element", "accept", "publicElem", "privateElem", "handleOpen", "elem", "apiGetBase", "data", "hasError", "createToast", "priv", "pub", "handleAccept", "publicKeyField", "toggleSecretButtons", "id", "action", "unlockButton", "lockButton", "copyButton", "initLockUnlock", "privateKeyModal", "unlock", "target", "plaintext", "isInputElement", "lock", "requestSessionKey", "privateKey", "apiPostForm", "res", "message", "isApiError", "initGetSessionKey", "handleClick", "pk", "initSecretsEdit", "handleSubmit", "event", "form", "initSecrets", "func", "initSecrets"]
 }
diff --git a/netbox_secrets/templates/netbox_secrets/inc/secret_actions.html b/netbox_secrets/templates/netbox_secrets/inc/secret_actions.html
index a0fb857..d2fd605 100644
--- a/netbox_secrets/templates/netbox_secrets/inc/secret_actions.html
+++ b/netbox_secrets/templates/netbox_secrets/inc/secret_actions.html
@@ -2,9 +2,7 @@
     <button class="btn btn-sm btn-success unlock-secret" secret-id="{{ object.pk }}" title="Unlock Secret"><i
             class="mdi mdi-lock"></i>
     </button>
-    <button class="btn btn-sm btn-outline-dark copy-secret d-none" secret-id="{{ object.pk }}" title="Copy to clipboard"
-            data-clipboard-target="#secret_{{ object.pk }}"><i class="mdi mdi-content-copy"></i>
-    </button>
+    <span secret-id="{{ object.pk }}" class="d-none">{% copy_content object.pk prefix="secret_" color="outline-dark" %}</span>
     <button class="btn btn-sm btn-danger lock-secret d-none" secret-id="{{ object.pk }}" title="Lock Secret">
         <i class="mdi mdi-lock-open"></i>
     </button>
diff --git a/setup.py b/setup.py
index 77d65c6..2ae413d 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,37 @@
 from setuptools import find_packages, setup
 
+description = """
+Enhance your secret management with encrypted storage and flexible, user-friendly features, seamlessly integrated into
+the NetBox environment.
+"""
+
+long_description="""
+# NetBox Secrets
+
+NetBox Secrets is a comprehensive overhaul of the original NetBox Secretstore plugin. It's designed to address the 
+limitations and maintenance challenges of the predecessor. This plugin introduces significant enhancements in terms of 
+flexibility and usability, ensuring seamless integration with the latest NetBox versions.
+
+## Key Features:
+
+- Employs RSA public key cryptography for secure storage of secrets in the database.
+- Allows secrets to be assigned to any object in NetBox, enhancing the plugin’s applicability and utility.
+- Enables assigning secrets to contacts, ideal for scenarios like linking SSH keys with specific individuals.
+- The redesigned interface is more intuitive and user-friendly, streamlining navigation and secret management.
+- Continuous testing with the latest NetBox releases ensures consistent compatibility and reliability.
+
+NetBox Secrets is committed to bridging the gap between robust security needs and an optimal user experience in NetBox
+environments.
+"""
+
 setup(
     name='netbox-secrets',
-    version='1.9.0',
-    description='Netbox Secrets',
-    long_description='A Secret store for NetBox',
+    version='1.9.1',
+    description=description,
+    long_description=long_description,
     url='https://github.com/Onemind-Services-LLC/netbox-secrets/',
-    author='Daniel Sheppard',
-    author_email='dans@dansheps.com',
+    author='Abhimanyu Saharan',
+    author_email='asaharan@onemindservices.com',
     license='Apache 2.0',
     install_requires=[
         'pycryptodome',


Test patch:
diff --git a/netbox_secrets/tests/test_filters.py b/netbox_secrets/tests/test_filters.py
index c4d1a4c..c6595db 100644
--- a/netbox_secrets/tests/test_filters.py
+++ b/netbox_secrets/tests/test_filters.py
@@ -1,4 +1,5 @@
 from django.test import TestCase
+from django.contrib.contenttypes.models import ContentType
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from netbox_secrets.filtersets import *
@@ -19,16 +20,12 @@ def setUpTestData(cls):
         )
         SecretRole.objects.bulk_create(roles)
 
-    def test_id(self):
+    def test_secret_role(self):
+        name = SecretRole.objects.all()
         params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-    def test_name(self):
-        name = SecretRole.objects.all()
         params = {'name': [name[0].name, name[1].name]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-    def test_slug(self):
         params = {'slug': ['secret-role-1', 'secret-role-2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
@@ -79,18 +76,28 @@ def setUpTestData(cls):
         for s in secrets:
             s.save()
 
-    def test_id(self):
-        params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-    def test_name(self):
-        name = Secret.objects.all()
-        params = {'name': [name[0].name, name[1].name, name[2].name, name[3].name, name[4].name, name[5].name]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+    def test_Secret(self):
+        secrets = Secret.objects.all()[:4]
+        params = {'id': [secrets[0].id, secrets[1].id, secrets[2].id, secrets[3].id]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        params = {'name': [secrets[0].name, secrets[1].name, secrets[2].name, secrets[3].name]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
     def test_role(self):
         roles = SecretRole.objects.all()[:2]
-        params = {'role_id': [roles[0].pk, roles[1].pk]}
+        params = {'role_id': [roles[0].id, roles[1].id]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'role': [roles[0].slug, roles[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+    def test_assigned_object(self):
+        params = {
+            'assigned_object_type': 'dcim.device',
+            'assigned_object_id': [Device.objects.first().pk],
+        }
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        params = {
+            'assigned_object_type': 'virtualization.virtualmachine',
+            'assigned_object_id': [VirtualMachine.objects.first().pk],
+        }
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
diff --git a/netbox_secrets/tests/test_models.py b/netbox_secrets/tests/test_models.py
index 42887d2..23c510e 100644
--- a/netbox_secrets/tests/test_models.py
+++ b/netbox_secrets/tests/test_models.py
@@ -50,18 +50,13 @@ def test_03_key_sizes(self):
         """
         rsa = RSA.generate(settings.PLUGINS_CONFIG['netbox_secrets'].get('public_key_size', 2048) - 256)
         small_key = rsa.publickey().exportKey('PEM')
-        try:
+        with self.assertRaises(ValidationError):
             UserKey(public_key=small_key).clean()
-            self.fail("UserKey.clean() did not fail with an undersized RSA key")
-        except ValidationError:
-            pass
-        rsa = RSA.generate(4096 + 256)  # Max size is 4096 (enforced by master_key_cipher field size)
+
+        rsa = RSA.generate(8192 + 256)  # Max size is 8192 (enforced by master_key_cipher field size)
         big_key = rsa.publickey().exportKey('PEM')
-        try:
+        with self.assertRaises(ValidationError):
             UserKey(public_key=big_key).clean()
-            self.fail("UserKey.clean() did not fail with an oversized RSA key")
-        except ValidationError:
-            pass
 
     def test_04_master_key_retrieval(self):
         """
@@ -161,7 +156,7 @@ def test_maximum_length(self):
         """
         Test encrypting a plaintext value of the maximum length.
         """
-        plaintext = '0123456789abcdef' * 4096
+        plaintext = '0123456789abcdef' * 8192
         plaintext = plaintext[:65535]  # 65,535 chars
         secret = Secret(plaintext=plaintext)
         secret.encrypt(self.secret_key)
diff --git a/test.sh b/test.sh
index da0144b..64261a5 100755
--- a/test.sh
+++ b/test.sh
@@ -8,9 +8,6 @@
 # exit when a command exits with an exit code != 0
 set -e
 
-# NETBOX_VARIANT is used by `Dockerfile` to determine the tag
-NETBOX_VARIANT="${1-latest}"
-
 # The docker compose command to use
 doco="docker compose --file docker-compose.yml"