mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 17:59:11 -06:00
migrate secrets to bootstrap 5 and deprecate jquery functions
This commit is contained in:
parent
726ab7fc05
commit
eb951fdaf1
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
24
netbox/project-static/src/global.d.ts
vendored
24
netbox/project-static/src/global.d.ts
vendored
@ -11,12 +11,15 @@ type APIAnswer<T> = {
|
|||||||
results: T[];
|
results: T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type APIError = {
|
type ErrorBase = {
|
||||||
error: string;
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type APIError = {
|
||||||
exception: string;
|
exception: string;
|
||||||
netbox_version: string;
|
netbox_version: string;
|
||||||
python_version: string;
|
python_version: string;
|
||||||
};
|
} & ErrorBase;
|
||||||
|
|
||||||
type APIObjectBase = {
|
type APIObjectBase = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -39,6 +42,23 @@ type APIReference = {
|
|||||||
_depth: number;
|
_depth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type APISecret = {
|
||||||
|
assigned_object: APIObjectBase;
|
||||||
|
assigned_object_id: number;
|
||||||
|
assigned_object_type: string;
|
||||||
|
created: string;
|
||||||
|
custom_fields: Record<string, unknown>;
|
||||||
|
display: string;
|
||||||
|
hash: string;
|
||||||
|
id: number;
|
||||||
|
last_updated: string;
|
||||||
|
name: string;
|
||||||
|
plaintext: Nullable<string>;
|
||||||
|
role: APIObjectBase;
|
||||||
|
tags: number[];
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface ObjectWithGroup extends APIObjectBase {
|
interface ObjectWithGroup extends APIObjectBase {
|
||||||
group: Nullable<APIReference>;
|
group: Nullable<APIReference>;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { initSpeedSelector, initForms } from './forms';
|
|||||||
import { initRackElevation } from './buttons';
|
import { initRackElevation } from './buttons';
|
||||||
import { initClipboard } from './clipboard';
|
import { initClipboard } from './clipboard';
|
||||||
import { initSearchBar } from './search';
|
import { initSearchBar } from './search';
|
||||||
// import { initGenerateKeyPair } from './secrets';
|
import { initGenerateKeyPair, initLockUnlock, initGetSessionKey } from './secrets';
|
||||||
import { getElements } from './util';
|
import { getElements } from './util';
|
||||||
|
|
||||||
const INITIALIZERS = [
|
const INITIALIZERS = [
|
||||||
@ -21,7 +21,9 @@ const INITIALIZERS = [
|
|||||||
initColorSelect,
|
initColorSelect,
|
||||||
initRackElevation,
|
initRackElevation,
|
||||||
initClipboard,
|
initClipboard,
|
||||||
// initGenerateKeyPair,
|
initGenerateKeyPair,
|
||||||
|
initLockUnlock,
|
||||||
|
initGetSessionKey,
|
||||||
] as (() => void)[];
|
] as (() => void)[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +37,6 @@ function initBootstrap(): void {
|
|||||||
new Tooltip(tooltip, { container: 'body', boundary: 'window' });
|
new Tooltip(tooltip, { container: 'body', boundary: 'window' });
|
||||||
}
|
}
|
||||||
for (const modal of getElements('[data-bs-toggle="modal"]')) {
|
for (const modal of getElements('[data-bs-toggle="modal"]')) {
|
||||||
// for (const modal of getElements('div.modal')) {
|
|
||||||
new Modal(modal);
|
new Modal(modal);
|
||||||
}
|
}
|
||||||
initMessageToasts();
|
initMessageToasts();
|
||||||
|
@ -1,47 +1,50 @@
|
|||||||
import { apiGetBase, getElements, isApiError } from './util';
|
import { Modal } from 'bootstrap';
|
||||||
|
import { apiGetBase, apiPostForm, getElements, isApiError, hasError } from './util';
|
||||||
|
import { createToast } from './toast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Initialize Generate Private Key Pair Elements.
|
||||||
* $('#generate_keypair').click(function() {
|
|
||||||
$('#new_keypair_modal').modal('show');
|
|
||||||
$.ajax({
|
|
||||||
url: netbox_api_path + 'secrets/generate-rsa-key-pair/',
|
|
||||||
type: 'GET',
|
|
||||||
dataType: 'json',
|
|
||||||
success: function (response, status) {
|
|
||||||
var public_key = response.public_key;
|
|
||||||
var private_key = response.private_key;
|
|
||||||
$('#new_pubkey').val(public_key);
|
|
||||||
$('#new_privkey').val(private_key);
|
|
||||||
},
|
|
||||||
error: function (xhr, ajaxOptions, thrownError) {
|
|
||||||
alert("There was an error generating a new key pair.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
*/
|
||||||
export function initGenerateKeyPair() {
|
export function initGenerateKeyPair() {
|
||||||
const element = document.getElementById('new_keypair_modal') as HTMLDivElement;
|
const element = document.getElementById('new_keypair_modal') as HTMLDivElement;
|
||||||
const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;
|
const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;
|
||||||
|
// If the elements are not loaded, stop.
|
||||||
|
if (element === null || accept === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const publicElem = element.querySelector<HTMLTextAreaElement>('textarea#new_pubkey');
|
const publicElem = element.querySelector<HTMLTextAreaElement>('textarea#new_pubkey');
|
||||||
const privateElem = element.querySelector<HTMLTextAreaElement>('textarea#new_privkey');
|
const privateElem = element.querySelector<HTMLTextAreaElement>('textarea#new_privkey');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Generate Private Key Pair Modal opening.
|
||||||
|
*/
|
||||||
function handleOpen() {
|
function handleOpen() {
|
||||||
|
// When the modal opens, set the `readonly` attribute on the textarea elements.
|
||||||
for (const elem of [publicElem, privateElem]) {
|
for (const elem of [publicElem, privateElem]) {
|
||||||
if (elem !== null) {
|
if (elem !== null) {
|
||||||
elem.setAttribute('readonly', '');
|
elem.setAttribute('readonly', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fetch the key pair from the API.
|
||||||
apiGetBase<APIKeyPair>('/api/secrets/generate-rsa-key-pair').then(data => {
|
apiGetBase<APIKeyPair>('/api/secrets/generate-rsa-key-pair').then(data => {
|
||||||
if (!isApiError(data)) {
|
if (!hasError(data)) {
|
||||||
|
// If key pair generation was successful, set the textarea elements' value to the generated
|
||||||
|
// values.
|
||||||
const { private_key: priv, public_key: pub } = data;
|
const { private_key: priv, public_key: pub } = data;
|
||||||
if (publicElem !== null && privateElem !== null) {
|
if (publicElem !== null && privateElem !== null) {
|
||||||
publicElem.value = pub;
|
publicElem.value = pub;
|
||||||
privateElem.value = priv;
|
privateElem.value = priv;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, show an error.
|
||||||
|
const toast = createToast('danger', 'Error', data.error);
|
||||||
|
toast.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Set the public key form field's value to the generated public key.
|
||||||
|
*/
|
||||||
function handleAccept() {
|
function handleAccept() {
|
||||||
const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;
|
const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;
|
||||||
if (publicElem !== null) {
|
if (publicElem !== null) {
|
||||||
@ -53,10 +56,147 @@ export function initGenerateKeyPair() {
|
|||||||
accept.addEventListener('click', handleAccept);
|
accept.addEventListener('click', handleAccept);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initLockUnlock() {
|
/**
|
||||||
for (const element of getElements<HTMLButtonElement>('button.unlock-secret')) {
|
* Toggle copy/lock/unlock button visibility based on the action occurring.
|
||||||
function handleClick() {
|
* @param id Secret ID.
|
||||||
const { secretId } = element.dataset;
|
* @param action Lock or Unlock, so we know which buttons to display.
|
||||||
}
|
*/
|
||||||
|
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}']`);
|
||||||
|
|
||||||
|
// If we're unlocking, hide the unlock button. Otherwise, show it.
|
||||||
|
if (unlockButton !== null) {
|
||||||
|
if (action === 'unlock') unlockButton.classList.add('d-none');
|
||||||
|
if (action === 'lock') unlockButton.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
// If we're unlocking, show the lock button. Otherwise, hide it.
|
||||||
|
if (lockButton !== null) {
|
||||||
|
if (action === 'unlock') lockButton.classList.remove('d-none');
|
||||||
|
if (action === 'lock') lockButton.classList.add('d-none');
|
||||||
|
}
|
||||||
|
// If we're unlocking, show the copy button. Otherwise, hide it.
|
||||||
|
if (copyButton !== null) {
|
||||||
|
if (action === 'unlock') copyButton.classList.remove('d-none');
|
||||||
|
if (action === 'lock') copyButton.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Lock & Unlock button event listeners & callbacks.
|
||||||
|
*/
|
||||||
|
export function initLockUnlock() {
|
||||||
|
const privateKeyModalElem = document.getElementById('privkey_modal');
|
||||||
|
if (privateKeyModalElem === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const privateKeyModal = new Modal(privateKeyModalElem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock a secret, or prompt the user for their private key, if a session key is not available.
|
||||||
|
*
|
||||||
|
* @param id Secret ID
|
||||||
|
*/
|
||||||
|
function unlock(id: string | null) {
|
||||||
|
const target = document.getElementById(`secret_${id}`);
|
||||||
|
if (typeof id === 'string' && id !== '') {
|
||||||
|
apiGetBase<APISecret>(`/api/secrets/secrets/${id}`).then(data => {
|
||||||
|
if (!hasError(data)) {
|
||||||
|
const { plaintext } = data;
|
||||||
|
// `plaintext` is the plain text value of the secret. If it is null, it has not been
|
||||||
|
// decrypted, likely due to a mission session key.
|
||||||
|
|
||||||
|
if (target !== null && plaintext !== null) {
|
||||||
|
// If `plaintext` is not null, we have the decrypted value. Set the target element's
|
||||||
|
// inner text to the decrypted value and toggle copy/lock button visibility.
|
||||||
|
target.innerText = plaintext;
|
||||||
|
toggleSecretButtons(id, 'unlock');
|
||||||
|
} else {
|
||||||
|
// Otherwise, we do _not_ have the decrypted value and need to prompt the user for
|
||||||
|
// their private RSA key, in order to get a session key. The session key is then sent
|
||||||
|
// as a cookie in future requests.
|
||||||
|
privateKeyModal.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.error.toLowerCase().includes('invalid session key')) {
|
||||||
|
// If, for some reason, a request was made but resulted in an API error that complains
|
||||||
|
// of a missing session key, prompt the user for their session key.
|
||||||
|
privateKeyModal.show();
|
||||||
|
} else {
|
||||||
|
// If we received an API error but it doesn't contain 'invalid session key', show the
|
||||||
|
// user an error message.
|
||||||
|
const toast = createToast('danger', 'Error', data.error);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Lock a secret and toggle visibility of the unlock button.
|
||||||
|
* @param id Secret ID
|
||||||
|
*/
|
||||||
|
function lock(id: string | null) {
|
||||||
|
if (typeof id === 'string' && id !== '') {
|
||||||
|
const target = document.getElementById(`secret_${id}`);
|
||||||
|
if (target !== null) {
|
||||||
|
// Obscure the inner text of the secret element.
|
||||||
|
target.innerText = '********';
|
||||||
|
}
|
||||||
|
// Toggle visibility of the copy/lock/unlock buttons.
|
||||||
|
toggleSecretButtons(id, 'lock');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of getElements<HTMLButtonElement>('button.unlock-secret')) {
|
||||||
|
element.addEventListener('click', () => unlock(element.getAttribute('secret-id')));
|
||||||
|
}
|
||||||
|
for (const element of getElements<HTMLButtonElement>('button.lock-secret')) {
|
||||||
|
element.addEventListener('click', () => lock(element.getAttribute('secret-id')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a session key from the API.
|
||||||
|
* @param privateKey RSA Private Key (valid JSON string)
|
||||||
|
*/
|
||||||
|
function requestSessionKey(privateKey: string) {
|
||||||
|
apiPostForm('/api/secrets/get-session-key/', { private_key: privateKey }).then(res => {
|
||||||
|
if (!hasError(res)) {
|
||||||
|
// If the response received was not an error, show the user a success message.
|
||||||
|
const toast = createToast('success', 'Session Key Received', 'You may now unlock secrets.');
|
||||||
|
toast.show();
|
||||||
|
} else {
|
||||||
|
// Otherwise, show the user an error message.
|
||||||
|
let message = res.error;
|
||||||
|
if (isApiError(res)) {
|
||||||
|
// If the error received was a standard API error containing a Python exception message,
|
||||||
|
// append it to the error.
|
||||||
|
message += `\n${res.exception}`;
|
||||||
|
}
|
||||||
|
const toast = createToast('danger', 'Failed to Retrieve Session Key', message);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Request Session Key Elements.
|
||||||
|
*/
|
||||||
|
export function initGetSessionKey() {
|
||||||
|
for (const element of getElements<HTMLButtonElement>('#request_session_key')) {
|
||||||
|
/**
|
||||||
|
* Send the user's input private key to the API to get a session key, which will be stored as
|
||||||
|
* a cookie for future requests.
|
||||||
|
*/
|
||||||
|
function handleClick() {
|
||||||
|
for (const pk of getElements<HTMLTextAreaElement>('#user_privkey')) {
|
||||||
|
requestSessionKey(pk.value);
|
||||||
|
// Clear the private key form field value.
|
||||||
|
pk.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.addEventListener('click', handleClick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import Cookie from 'cookie';
|
import Cookie from 'cookie';
|
||||||
|
|
||||||
export function isApiError(data: Record<string, unknown>): data is APIError {
|
export function isApiError(data: Record<string, unknown>): data is APIError {
|
||||||
|
return 'error' in data && 'exception' in data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasError(data: Record<string, unknown>): data is ErrorBase {
|
||||||
return 'error' in data;
|
return 'error' in data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,13 +37,55 @@ export function getCsrfToken(): string {
|
|||||||
|
|
||||||
export async function apiGetBase<T extends Record<string, unknown>>(
|
export async function apiGetBase<T extends Record<string, unknown>>(
|
||||||
url: string,
|
url: string,
|
||||||
): Promise<T | APIError> {
|
): Promise<T | ErrorBase | APIError> {
|
||||||
const token = getCsrfToken();
|
const token = getCsrfToken();
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'X-CSRFToken': token },
|
headers: { 'X-CSRFToken': token },
|
||||||
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
|
const contentType = res.headers.get('Content-Type');
|
||||||
|
if (typeof contentType === 'string' && contentType.includes('text')) {
|
||||||
|
const error = await res.text();
|
||||||
|
return { error } as ErrorBase;
|
||||||
|
}
|
||||||
|
|
||||||
const json = (await res.json()) as T | APIError;
|
const json = (await res.json()) as T | APIError;
|
||||||
|
if (!res.ok && Array.isArray(json)) {
|
||||||
|
const error = json.join('\n');
|
||||||
|
return { error } as ErrorBase;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiPostForm<
|
||||||
|
T extends Record<string, unknown>,
|
||||||
|
R extends Record<string, unknown>
|
||||||
|
>(url: string, data: T): Promise<R | ErrorBase | APIError> {
|
||||||
|
const token = getCsrfToken();
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
body.append(k, String(v));
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
headers: { 'X-CSRFToken': token },
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentType = res.headers.get('Content-Type');
|
||||||
|
if (typeof contentType === 'string' && contentType.includes('text')) {
|
||||||
|
let error = await res.text();
|
||||||
|
if (contentType.includes('text/html')) {
|
||||||
|
error = res.statusText;
|
||||||
|
}
|
||||||
|
return { error } as ErrorBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (await res.json()) as R | APIError;
|
||||||
|
if (!res.ok && 'detail' in json) {
|
||||||
|
return { error: json.detail as string } as ErrorBase;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +95,7 @@ export async function apiGetBase<T extends Record<string, unknown>>(
|
|||||||
*/
|
*/
|
||||||
export async function getApiData<T extends APIObjectBase>(
|
export async function getApiData<T extends APIObjectBase>(
|
||||||
url: string,
|
url: string,
|
||||||
): Promise<APIAnswer<T> | APIError> {
|
): Promise<APIAnswer<T> | ErrorBase | APIError> {
|
||||||
return await apiGetBase<APIAnswer<T>>(url);
|
return await apiGetBase<APIAnswer<T>>(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,26 +2,26 @@
|
|||||||
<div class="modal-dialog modal-md" role="document">
|
<div class="modal-dialog modal-md" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<h5 class="modal-title" id="privkey_modal_title">
|
||||||
<h4 class="modal-title" id="privkey_modal_title">
|
|
||||||
<span class="mdi mdi-lock" aria-hidden="true"></span>
|
<span class="mdi mdi-lock" aria-hidden="true"></span>
|
||||||
Enter your private RSA key
|
Enter Private RSA Key
|
||||||
</h4>
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p class="small text-muted">
|
||||||
You do not have an active session key. To request one, please provide your private RSA key below.
|
You do not have an active session key. To request one, please provide your private RSA key below.
|
||||||
Once retrieved, your session key will be saved for future requests.
|
Once retrieved, your session key will be saved for future requests.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea class="form-control" id="user_privkey" style="height: 300px;"></textarea>
|
<textarea class="form-control font-monospace" id="user_privkey" style="height: 300px;"></textarea>
|
||||||
</div>
|
|
||||||
<div class="form-group text-right noprint">
|
|
||||||
<button id="request_session_key" class="btn btn-primary" data-dismiss="modal">
|
|
||||||
Request session key
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer float-end">
|
||||||
|
<button id="request_session_key" class="btn btn-primary" data-bs-dismiss="modal">
|
||||||
|
Request Session Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,15 +53,15 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">Secret</div>
|
<div class="col-md-2">Secret</div>
|
||||||
<div class="col-md-6" id="secret_{{ object.pk }}">********</div>
|
<div class="col-md-6"><code id="secret_{{ object.pk }}">********</code></div>
|
||||||
<div class="col-md-4 text-right noprint">
|
<div class="col-md-4 text-right noprint">
|
||||||
<button class="btn btn-sm btn-success unlock-secret" secret-id="{{ object.pk }}">
|
<button class="btn btn-sm btn-success unlock-secret" secret-id="{{ object.pk }}">
|
||||||
<i class="mdi mdi-lock"></i> Unlock
|
<i class="mdi mdi-lock"></i> Unlock
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-default copy-secret collapse" secret-id="{{ object.pk }}" data-clipboard-target="#secret_{{ object.pk }}">
|
<button class="btn btn-sm btn-outline-dark copy-secret d-none" secret-id="{{ object.pk }}" data-clipboard-target="#secret_{{ object.pk }}">
|
||||||
<i class="mdi mdi-content-copy"></i> Copy
|
<i class="mdi mdi-content-copy"></i> Copy
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-danger lock-secret collapse" secret-id="{{ object.pk }}">
|
<button class="btn btn-sm btn-danger lock-secret d-none" secret-id="{{ object.pk }}">
|
||||||
<i class="mdi mdi-lock-open"></i> Lock
|
<i class="mdi mdi-lock-open"></i> Lock
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,3 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% include 'secrets/inc/private_key_modal.html' %}
|
{% include 'secrets/inc/private_key_modal.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
@ -3,33 +3,35 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<li><a href="{% url 'secrets:secretrole_list' %}">Secret Roles</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'secrets:secretrole_list' %}">Secret Roles</a></li>
|
||||||
<li>{{ object }}</li>
|
<li class="breadcrumb-item">{{ object }}</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<h5 class="card-header">
|
||||||
<strong>Secret Role</strong>
|
Secret Role
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Secrets</th>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'secrets:secret_list' %}?role_id={{ object.pk }}">{{ secrets_table.rows|length }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body attr-table">
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>{{ object.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>{{ object.description|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Secrets</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'secrets:secret_list' %}?role_id={{ object.pk }}">{{ secrets_table.rows|length }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
@ -40,15 +42,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<h5 class="card-header">
|
||||||
<strong>Secrets</strong>
|
Secrets
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'inc/table.html' with table=secrets_table %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/table.html' with table=secrets_table %}
|
|
||||||
{% if perms.secrets.add_secret %}
|
{% if perms.secrets.add_secret %}
|
||||||
<div class="panel-footer text-right noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'secrets:secret_add' %}?role={{ object.pk }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'secrets:secret_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add secret
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Secret
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
Reference in New Issue
Block a user