Compare commits

...

3 Commits

Author SHA1 Message Date
Jeremy Stretch
353e627bf2 Merge 8adc0bd374 into 20c260b126 2025-12-04 20:30:42 +00:00
Jeremy Stretch
20c260b126 Closes #20572: Update all development frontend dependencies (#20909)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-12-04 09:00:57 -08:00
Jeremy Stretch
7bca9f5d6d Closes #20917: Show example API usage for tokens (#20918)
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-03 17:37:40 -06:00
15 changed files with 1211 additions and 1110 deletions

View File

@@ -25,10 +25,12 @@ from extras.models import Bookmark
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.ui import layout
from netbox.views import generic
from users import forms
from users.models import UserConfig
from users.tables import TokenTable
from users.ui.panels import TokenExamplePanel, TokenPanel
from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
@@ -342,12 +344,21 @@ class UserTokenListView(LoginRequiredMixin, View):
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
layout = layout.SimpleLayout(
left_panels=[
TokenPanel(),
],
right_panels=[
TokenExamplePanel(),
],
)
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
return render(request, 'account/token.html', {
'object': token,
'layout': self.layout,
})

View File

@@ -1,3 +0,0 @@
dist
node_modules
.cache

View File

@@ -1,53 +0,0 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:import/typescript",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"arrowFunctions": true
}
},
"plugins": ["@typescript-eslint", "prettier"],
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {}
}
},
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"no-unused-vars": "off",
"no-inner-declarations": "off",
"comma-dangle": ["error", "always-multiline"],
"global-require": "off",
"import/no-dynamic-require": "off",
"import/prefer-default-export": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-interface": [
"error",
{
"allowSingleExtends": true
}
]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,86 @@
import { defineConfig, globalIgnores } from "eslint/config";
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import prettier from "eslint-plugin-prettier";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default defineConfig([
globalIgnores(['**/dist', '**/node_modules', '**/.cache']),
{
extends: fixupConfigRules(
compat.extends(
'eslint:recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'prettier',
),
),
plugins: {
'@typescript-eslint': fixupPluginRules(typescriptEslint),
prettier: fixupPluginRules(prettier),
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
arrowFunctions: true,
},
},
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'no-unused-vars': 'off',
'no-inner-declarations': 'off',
'comma-dangle': ['error', 'always-multiline'],
'global-require': 'off',
'import/no-dynamic-require': 'off',
'import/prefer-default-export': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
},
},
]);

View File

@@ -1,6 +1,7 @@
{
"name": "netbox",
"version": "4.4.0",
"type": "module",
"version": "4.5.0",
"main": "dist/netbox.js",
"license": "Apache-2.0",
"private": true,
@@ -8,14 +9,14 @@
"netbox-graphiql"
],
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
"bundle:scripts": "node bundle.js --scripts",
"bundle": "node bundle.cjs",
"bundle:styles": "node bundle.cjs --styles",
"bundle:scripts": "node bundle.cjs --scripts",
"format": "yarn format:scripts && yarn format:styles",
"format:scripts": "prettier -w src/**/*.ts",
"format:styles": "prettier -w styles/**/*.scss",
"validate": "yarn validate:types && yarn validate:lint",
"validate:lint": "eslint -c .eslintrc ./src/**/*.ts",
"validate:lint": "eslint ./src/**/*.ts",
"validate:types": "tsc --noEmit",
"validate:formatting": "yarn validate:formatting:scripts && yarn validate:formatting:styles",
"validate:formatting:styles": "prettier -c styles/**/*.scss",
@@ -36,20 +37,24 @@
"typeface-roboto-mono": "1.1.13"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.1",
"@types/bootstrap": "5.2.10",
"@types/cookie": "^0.6.0",
"@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"esbuild": "^0.25.11",
"@types/cookie": "^1.0.0",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"esbuild": "^0.27.0",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "<9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.1",
"prettier": "^3.3.3",
"typescript": "<5.5"
"globals": "^16.5.0",
"prettier": "^3.7.3",
"typescript": "^5.9.3"
},
"resolutions": {
"@types/bootstrap/**/@popperjs/core": "^2.11.6"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{% extends 'ui/panels/_base.html' %}
{% block panel_content %}
<div id="token-example" class="card-body font-monospace">curl -X GET \<br />
-H "Authorization: {{ object.get_auth_header_prefix }}<mark>&lt;TOKEN&gt;</mark>" \<br />
-H "Content-Type: application/json" \<br />
-H "Accept: application/json; indent=4" \<br />
{{ request.scheme }}://{{ request.get_host }}{% url "api-status" %}</div>
{% endblock panel_content %}

View File

@@ -1,73 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Token" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Version" %}</th>
<td>{{ object.version }}</td>
</tr>
{% if object.version == 1 %}
<tr>
<th scope="row">{% trans "Token" %}</th>
<td>{{ object.partial }}</td>
</tr>
{% else %}
<tr>
<th scope="row">{% trans "Key" %}</th>
<td>{{ object }}</td>
</tr>
<tr>
<th scope="row">{% trans "Pepper ID" %}</th>
<td>{{ object.pepper_id }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "User" %}</th>
<td>
<a href="{% url 'users:user' pk=object.user.pk %}">{{ object.user }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Write enabled" %}</th>
<td>{% checkmark object.write_enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "Expires" %}</th>
<td>{{ object.expires|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last used" %}</th>
<td>{{ object.last_used|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Allowed IPs" %}</th>
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -201,6 +201,15 @@ class Token(models.Model):
"""
return self.enabled and not self.is_expired
def get_auth_header_prefix(self):
"""
Return the HTTP Authorization header prefix for this token.
"""
if self.v1:
return 'Token '
if self.v2:
return f'Bearer {TOKEN_PREFIX}{self.key}.'
def clean(self):
super().clean()

View File

25
netbox/users/ui/panels.py Normal file
View File

@@ -0,0 +1,25 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, attrs, panels
class TokenPanel(panels.ObjectAttributesPanel):
version = attrs.NumericAttr('version')
key = attrs.TextAttr('key')
token = attrs.TextAttr('partial')
pepper_id = attrs.NumericAttr('pepper_id')
user = attrs.RelatedObjectAttr('user', linkify=True)
description = attrs.TextAttr('description')
enabled = attrs.BooleanAttr('enabled')
write_enabled = attrs.BooleanAttr('write_enabled')
expires = attrs.TextAttr('expires')
last_used = attrs.TextAttr('last_used')
allowed_ips = attrs.TextAttr('allowed_ips')
class TokenExamplePanel(panels.Panel):
template_name = 'users/panels/token_example.html'
title = _('Example Usage')
actions = [
actions.CopyContent('token-example')
]

View File

@@ -3,7 +3,9 @@ from django.db.models import Count
from core.models import ObjectChange
from core.tables import ObjectChangeTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from netbox.ui import layout
from netbox.views import generic
from users.ui import panels
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
@@ -26,6 +28,14 @@ class TokenListView(generic.ObjectListView):
@register_model_view(Token)
class TokenView(generic.ObjectView):
queryset = Token.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.TokenPanel(),
],
right_panels=[
panels.TokenExamplePanel(),
],
)
@register_model_view(Token, 'add', detail=False)