Compare commits

..

8 Commits

Author SHA1 Message Date
Corentin Thomasset
8d09086e78 chore(release): 2.15.0 2022-12-16 21:59:47 +01:00
Corentin Thomasset
acf8bc11db fix(tool-card): correct text color on light mode for card description 2022-12-16 21:57:23 +01:00
Corentin Thomasset
71e98e93e5 feat(search-bar): better search back result 2022-12-16 21:44:54 +01:00
Corentin Thomasset
1b5d4e72bd refactor(search-bar): improved tool fuzzy search 2022-12-16 18:10:50 +01:00
Corentin Thomasset
8476cf319b fix(integer-base-converter): handle non-decimal char and better error message 2022-12-07 21:52:24 +01:00
Corentin Thomasset
0ff853437b chore(release): 2.14.1 2022-11-23 22:00:08 +01:00
Corentin Thomasset
39c8f92065 chore(release): 2.14.0 2022-11-23 22:00:01 +01:00
Corentin Thomasset
35b5187119 feat(new-tool): chmod calculator 2022-11-23 21:57:38 +01:00
25 changed files with 439 additions and 17852 deletions

View File

@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.15.0](https://github.com/CorentinTh/it-tools/compare/v2.14.1...v2.15.0) (2022-12-16)
### Features
* **search-bar:** better search back result ([71e98e9](https://github.com/CorentinTh/it-tools/commit/71e98e93e5752cba934f67d679088524c4d3d2ad))
### Bug Fixes
* **integer-base-converter:** handle non-decimal char and better error message ([8476cf3](https://github.com/CorentinTh/it-tools/commit/8476cf319b7ebae87c7928592604a54833ac56ef))
* **tool-card:** correct text color on light mode for card description ([acf8bc1](https://github.com/CorentinTh/it-tools/commit/acf8bc11dbab85ab361edbe400ebbe5e52a11b89))
### Refactors
* **search-bar:** improved tool fuzzy search ([1b5d4e7](https://github.com/CorentinTh/it-tools/commit/1b5d4e72bdb222dd721a1e484c3e5d73bb62d2b1))
### [2.14.1](https://github.com/CorentinTh/it-tools/compare/v2.14.0...v2.14.1) (2022-11-23)
## [2.14.0](https://github.com/CorentinTh/it-tools/compare/v2.13.0...v2.14.0) (2022-11-23)
### Features
* **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267))
## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14)

17251
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2.13.0",
"version": "2.15.0",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -45,9 +45,9 @@
"crypto-js": "^4.1.1",
"date-fns": "^2.29.3",
"figue": "^1.2.0",
"fuse.js": "^6.6.2",
"highlight.js": "^11.6.0",
"json5": "^2.2.1",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"mathjs": "^10.6.4",
"mime-types": "^2.1.35",

7
pnpm-lock.yaml generated
View File

@@ -38,6 +38,7 @@ specifiers:
eslint-plugin-import: ^2.26.0
eslint-plugin-vue: ^8.7.1
figue: ^1.2.0
fuse.js: ^6.6.2
highlight.js: ^11.6.0
jsdom: ^19.0.0
json5: ^2.2.1
@@ -81,6 +82,7 @@ dependencies:
crypto-js: 4.1.1
date-fns: 2.29.3
figue: 1.2.0
fuse.js: 6.6.2
highlight.js: 11.6.0
json5: 2.2.1
lodash: 4.17.21
@@ -4054,6 +4056,11 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
/fuse.js/6.6.2:
resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
engines: {node: '>=10'}
dev: false
/gensync/1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}

View File

@@ -1,28 +1,30 @@
<script lang="ts" setup>
import { useFuzzySearch } from '@/composable/fuzzySearch';
import { tools } from '@/tools';
import type { ITool } from '@/tools/tool';
import { SearchRound } from '@vicons/material';
import { useMagicKeys, whenever } from '@vueuse/core';
import { deburr } from 'lodash';
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { useRouter } from 'vue-router';
import SearchBarItem from './SearchBarItem.vue';
const router = useRouter();
const queryString = ref('');
const cleanString = (s: string) => deburr(s.trim().toLowerCase());
const { searchResult } = useFuzzySearch({
search: queryString,
data: tools,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
});
const searchableTools = tools.map(({ name, description, keywords, path }) => ({
searchableText: [name, description, ...keywords].map(cleanString).join(' '),
path,
name,
}));
const toolToOption = (tool: ITool) => ({ label: tool.name, value: tool.path, tool });
const options = computed(() => {
const query = cleanString(queryString.value);
if (queryString.value === '') {
return tools.map(toolToOption);
}
return searchableTools
.filter(({ searchableText }) => searchableText.includes(query))
.map(({ name, path }) => ({ label: name, value: path }));
return searchResult.value.map(toolToOption);
});
function onSelect(path: string) {
@@ -44,6 +46,10 @@ const keys = useMagicKeys({
whenever(keys.ctrl_k, () => {
focusTarget.value.focus();
});
function renderOption({ tool }: { tool: ITool }) {
return h(SearchBarItem, { tool });
}
</script>
<template>
@@ -51,8 +57,10 @@ whenever(keys.ctrl_k, () => {
<n-auto-complete
v-model:value="queryString"
:options="options"
:input-props="{ autocomplete: 'disabled' }"
:on-select="onSelect"
:on-select="(value) => onSelect(String(value))"
:render-label="renderOption"
:default-value="'aa'"
:get-show="() => true"
>
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
<n-input
@@ -61,6 +69,7 @@ whenever(keys.ctrl_k, () => {
clearable
placeholder="Search a tool... [Ctrl + K]"
:value="slotValue"
:input-props="{ autocomplete: 'disabled' }"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@@ -74,8 +83,4 @@ whenever(keys.ctrl_k, () => {
</div>
</template>
<style lang="less" scoped>
// ::v-deep(.n-input__border) {
// border: none;
// }
</style>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import type { ITool } from '@/tools/tool';
import { toRefs } from 'vue';
const props = defineProps<{ tool: ITool }>();
const { tool } = toRefs(props);
</script>
<template>
<div class="search-bar-item">
<n-icon class="icon" :component="tool.icon" />
<div>
<div class="name">{{ tool.name }}</div>
<div class="description">{{ tool.description }}</div>
</div>
</div>
</template>
<style lang="less" scoped>
.search-bar-item {
padding: 10px;
display: flex;
flex-direction: row;
align-items: center;
.icon {
font-size: 30px;
margin-right: 10px;
opacity: 0.7;
}
.name {
font-weight: bold;
font-size: 15px;
line-height: 1;
margin-bottom: 5px;
}
.description {
opacity: 0.7;
line-height: 1;
}
}
</style>

View File

@@ -50,7 +50,7 @@ a {
.icon {
opacity: 0.6;
color: #ffffff;
color: v-bind('theme.textColorBase');
}
.title {
@@ -59,7 +59,7 @@ a {
.description {
opacity: 0.6;
color: #ffffff;
color: v-bind('theme.textColorBase');
margin: 5px 0;
}
}

View File

@@ -0,0 +1,23 @@
import { get, type MaybeRef } from '@vueuse/core';
import Fuse from 'fuse.js';
import { computed } from 'vue';
export { useFuzzySearch };
function useFuzzySearch<Data>({
search,
data,
options = {},
}: {
search: MaybeRef<string>;
data: Data[];
options?: Fuse.IFuseOptions<Data>;
}) {
const fuse = new Fuse(data, options);
const searchResult = computed(() => {
return fuse.search(get(search)).map(({ item }) => item);
});
return { searchResult };
}

View File

@@ -53,10 +53,11 @@ import {
NTooltip,
NUpload,
NUploadDragger,
NPopover,
NCheckbox,
} from 'naive-ui';
const components = [
NCheckbox,
NDynamicInput,
NDatePicker,
NCode,
@@ -110,7 +111,6 @@ const components = [
NIcon,
NSwitch,
NCollapseTransition,
NPopover,
];
export const naive = create({ components });

View File

@@ -6,6 +6,12 @@ export const lightThemeOverrides: GlobalThemeOverrides = {
},
Layout: { color: '#f1f5f9' },
AutoComplete: {
peers: {
InternalSelectMenu: { height: '500px' },
},
},
};
export const darkThemeOverrides: GlobalThemeOverrides = {
@@ -16,6 +22,12 @@ export const darkThemeOverrides: GlobalThemeOverrides = {
primaryColorSuppl: '#36AD6AFF',
},
AutoComplete: {
peers: {
InternalSelectMenu: { height: '500px', color: '#1e1e1e' },
},
},
Menu: {
itemHeight: '32px',
},

View File

@@ -0,0 +1,68 @@
import { expect, describe, it } from 'vitest';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
describe('chmod-calculator', () => {
describe('computeChmodOctalRepresentation', () => {
it('get the octal representation from permissions', () => {
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: true, write: true, execute: true },
group: { read: true, write: true, execute: true },
public: { read: true, write: true, execute: true },
},
}),
).to.eql('777');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
},
}),
).to.eql('000');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: true },
public: { read: true, write: false, execute: true },
},
}),
).to.eql('235');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: true, write: false, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: false, execute: true },
},
}),
).to.eql('421');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: false, execute: true },
group: { read: false, write: true, execute: false },
public: { read: true, write: false, execute: false },
},
}),
).to.eql('124');
expect(
computeChmodOctalRepresentation({
permissions: {
owner: { read: false, write: true, execute: false },
group: { read: false, write: true, execute: false },
public: { read: false, write: true, execute: false },
},
}),
).to.eql('222');
});
});
});

View File

@@ -0,0 +1,17 @@
import _ from 'lodash';
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
export { computeChmodOctalRepresentation };
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
const permissionValue = { read: 4, write: 2, execute: 1 };
const getGroupPermissionValue = (permission: GroupPermissions) =>
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0);
return [
getGroupPermissionValue(permissions.owner),
getGroupPermissionValue(permissions.group),
getGroupPermissionValue(permissions.public),
].join('');
}

View File

@@ -0,0 +1,10 @@
export type Scope = 'read' | 'write' | 'execute';
export type Group = 'owner' | 'group' | 'public';
export type GroupPermissions = {
[k in Scope]: boolean;
};
export type Permissions = {
[k in Group]: GroupPermissions;
};

View File

@@ -0,0 +1,83 @@
<template>
<div>
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
<thead>
<tr>
<th class="text-center" scope="col"></th>
<th class="text-center" scope="col">Owner (u)</th>
<th class="text-center" scope="col">Group (g)</th>
<th class="text-center" scope="col">Public (o)</th>
</tr>
</thead>
<tbody>
<tr v-for="{ scope, title } of scopes" :key="scope">
<td class="line-header">{{ title }}</td>
<td v-for="group of groups" :key="group" class="text-center">
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
</td>
</tr>
</tbody>
</n-table>
<div class="octal-result">
{{ octal }}
</div>
<input-copyable :value="`chmod ${octal} path`" readonly style="margin-bottom: 5px" />
</div>
</template>
<script setup lang="ts">
import { useThemeVars } from 'naive-ui';
import { computed, ref } from 'vue';
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
import InputCopyable from '../../components/InputCopyable.vue';
import type { Group, Scope } from './chmod-calculator.types';
const themeVars = useThemeVars();
const scopes: { scope: Scope; title: string }[] = [
{ scope: 'read', title: 'Read (4)' },
{ scope: 'write', title: 'Write (2)' },
{ scope: 'execute', title: 'Execute (1)' },
];
const groups: Group[] = ['owner', 'group', 'public'];
const permissions = ref({
owner: { read: false, write: false, execute: false },
group: { read: false, write: false, execute: false },
public: { read: false, write: false, execute: false },
});
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
</script>
<style lang="less" scoped>
.octal-result {
text-align: center;
font-size: 50px;
font-family: monospace;
color: v-bind('themeVars.primaryColor');
margin: 20px 0;
}
.permission-table {
td,
th {
padding: 15px;
@media screen and (max-width: 600px) {
padding: 5px;
}
}
}
.line-header {
font-weight: bold;
text-align: right;
max-width: 80px;
}
.text-center {
text-align: center;
}
</style>

View File

@@ -0,0 +1,22 @@
import { FileInvoice } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Chmod calculator',
path: '/chmod-calculator',
description: 'Compute your chmod permissions and commands with this online chmod calculator.',
keywords: [
'chmod',
'calculator',
'file',
'permission',
'files',
'directory',
'folder',
'recursive',
'generator',
'octal',
],
component: () => import('./chmod-calculator.vue'),
icon: FileInvoice,
});

View File

@@ -1,7 +1,7 @@
import { LockOpen } from '@vicons/tabler';
import type { ToolCategory } from './tool';
import { tool as jwtParser } from './jwt-parser';
import { tool as chmodCalculator } from './chmod-calculator';
import { tool as mimeTypes } from './mime-types';
import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator';
import { tool as base64FileConverter } from './base64-file-converter';
@@ -68,7 +68,6 @@ export const toolsByCategory: ToolCategory[] = [
metaTagGenerator,
otpCodeGeneratorAndValidator,
mimeTypes,
jwtParser,
],
},
{
@@ -79,7 +78,7 @@ export const toolsByCategory: ToolCategory[] = [
{
name: 'Development',
icon: LockOpen,
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify],
components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator],
},
{
name: 'Math',

View File

@@ -7,7 +7,7 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa
.reverse()
.reduce((carry: number, digit: string, index: number) => {
if (!fromRange.includes(digit)) {
throw new Error('Invalid digit `' + digit + '` for base ' + fromBase + '.');
throw new Error('Invalid digit "' + digit + '" for base ' + fromBase + '.');
}
return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index));
}, 0);

View File

@@ -4,7 +4,7 @@
<div v-if="styleStore.isSmallScreen">
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
<n-input-number v-model:value="inputNumber" min="0" style="width: 100%" />
<n-input v-model:value="input" style="width: 100%" :status="error ? 'error' : undefined" />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
@@ -14,51 +14,65 @@
<n-input-group v-else>
<n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label>
<n-input-number v-model:value="inputNumber" min="0" />
<n-input v-model:value="input" :status="error ? 'error' : undefined" />
<n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label>
<n-input-number v-model:value="inputBase" max="64" min="2" />
</n-input-group>
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
<n-divider />
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
<input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 2 })" readonly />
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
readonly
placeholder="Binary version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
<input-copyable :value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 8 })" readonly />
<input-copyable
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
readonly
placeholder="Octal version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
<input-copyable
:value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 10 })"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
readonly
placeholder="Decimal version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
<input-copyable
:value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 16 })"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
readonly
placeholder="Decimal version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
<input-copyable
:value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: 64 })"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
readonly
placeholder="Base64 version will be here..."
/>
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
<n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
<input-copyable
:value="convertBase({ value: String(inputNumber), fromBase: inputBase, toBase: outputBase })"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
readonly
:placeholder="`Base ${outputBase} will be here...`"
/>
</n-input-group>
</n-card>
@@ -66,16 +80,31 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useStyleStore } from '@/stores/style.store';
import { getErrorMessageIfThrows } from '@/utils/error';
import { convertBase } from './integer-base-converter.model';
import InputCopyable from '../../components/InputCopyable.vue';
const styleStore = useStyleStore();
const inputNumber = ref(42);
const input = ref('42');
const inputBase = ref(10);
const outputBase = ref(42);
function errorlessConvert(...args: Parameters<typeof convertBase>) {
try {
return convertBase(...args);
} catch (err) {
return '';
}
}
const error = computed(() =>
getErrorMessageIfThrows(() =>
convertBase({ value: input.value, fromBase: inputBase.value, toBase: outputBase.value }),
),
);
</script>
<style lang="less" scoped>

View File

@@ -1,29 +0,0 @@
<template>
<n-space>
<em>{{ claim }}</em>
<span v-if="label.label !== claim">
<n-popover placement="right" trigger="hover">
<template #trigger>
<n-icon :component="InfoCircle" trigger />
</template>
{{ label.label }}
<template v-if="label.ref !== ''" #footer> {{ label.ref }} </template>
</n-popover>
</span>
</n-space>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { InfoCircle } from '@vicons/tabler';
import { get_claim_label } from './jwt-parser.service';
const props = defineProps({
claim: {
type: String,
default: '',
},
});
const label = computed(() => get_claim_label(props.claim ? props.claim : ''));
</script>

View File

@@ -1,11 +0,0 @@
import { ArrowsShuffle } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'JWT parser',
path: '/jwt-parser',
description: '',
keywords: ['jwt', 'parser'],
component: () => import('./jwt-parser.vue'),
icon: ArrowsShuffle,
});

View File

@@ -1,429 +0,0 @@
import jwt_decode, { InvalidTokenError } from 'jwt-decode';
interface JWT {
header: Map<string, unknown>;
payload: Map<string, unknown>;
}
export function safe_jwt_decode(raw_jwt: string): JWT {
try {
const header = jwt_decode(raw_jwt, { header: true }) as Map<string, unknown>;
const payload = jwt_decode(raw_jwt) as Map<string, unknown>;
return { header: header, payload: payload };
} catch (e) {
if (e instanceof InvalidTokenError) {
return { header: new Map<string, unknown>(), payload: new Map<string, unknown>() };
} else {
throw e;
}
}
}
export function get_claim_label(claim: string): { label: string; ref: string } {
const infos = STANDARD_CLAIMS.find((info) => info.name === claim);
if (infos) {
return { label: infos.long_name, ref: infos.ref };
}
switch (claim) {
case 'typ':
return { label: 'Type', ref: '' };
case 'alg':
return { label: 'Algorithm', ref: '' };
}
return { label: claim, ref: '' };
}
export function parse_claim_value(claim: string, value: unknown): { value: unknown; extension?: unknown } {
switch (claim) {
case 'exp':
case 'nbf':
case 'iat': {
// Convert to milliseconds, JWT specs says it should be in seconds, JS
// works with milliseconds
value = typeof value === 'string' ? parseInt(value) : value;
const date = new Date((value as number) * 1000);
return { value: `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, extension: value };
}
case 'alg':
return { value: AlgorithmKeyDescriptionMapping[value as string], extension: value };
default:
if (typeof value === 'boolean') {
// Perhaps there's a better way to do this?
return { value: value ? 'true' : 'false' };
}
return { value: value };
}
}
// From https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
const AlgorithmKeyDescriptionMapping: { [k: string]: string } = {
HS256: 'HMAC using SHA-256',
HS384: 'HMAC using SHA-384',
HS512: 'HMAC using SHA-512',
RS256: 'RSASSA-PKCS1-v1_5 using SHA-256',
RS384: 'RSASSA-PKCS1-v1_5 using SHA-384',
RS512: 'RSASSA-PKCS1-v1_5 using SHA-512',
ES256: 'ECDSA using P-256 and SHA-256',
ES384: 'ECDSA using P-384 and SHA-384',
ES512: 'ECDSA using P-521 and SHA-512',
PS256: 'RSASSA-PSS using SHA-256 and MGF1 with SHA-256',
PS384: 'RSASSA-PSS using SHA-384 and MGF1 with SHA-384',
PS512: 'RSASSA-PSS using SHA-512 and MGF1 with SHA-512',
none: 'No digital signature or MAC performed',
};
// List extracted from IANA: https://www.iana.org/assignments/jwt/jwt.xhtml
const STANDARD_CLAIMS = [
{
name: 'iss',
long_name: 'Issuer',
ref: '[RFC7519 - Section 4.1.1]',
},
{
name: 'sub',
long_name: 'Subject',
ref: '[RFC7519 - Section 4.1.2]',
},
{
name: 'aud',
long_name: 'Audience',
ref: '[RFC7519 - Section 4.1.3]',
},
{
name: 'exp',
long_name: 'Expiration Time',
ref: '[RFC7519 - Section 4.1.4]',
},
{
name: 'nbf',
long_name: 'Not Before',
ref: '[RFC7519 - Section 4.1.5]',
},
{
name: 'iat',
long_name: 'Issued At',
ref: '[RFC7519 - Section 4.1.6]',
},
{
name: 'jti',
long_name: 'JWT ID',
ref: '[RFC7519 - Section 4.1.7]',
},
{
name: 'name',
long_name: 'Full name',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'given_name',
long_name: 'Given name(s) or first name(s)',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'family_name',
long_name: 'Surname(s) or last name(s)',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'middle_name',
long_name: 'Middle name(s)',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'nickname',
long_name: 'Casual name',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'preferred_username',
long_name: 'Shorthand name by which the End-User wishes to be referred to',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'profile',
long_name: 'Profile page URL',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'picture',
long_name: 'Profile picture URL',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'website',
long_name: 'Web page or blog URL',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'email',
long_name: 'Preferred e-mail address',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'email_verified',
long_name: 'True if the e-mail address has been verified; otherwise false',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'gender',
long_name: 'Gender',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'birthdate',
long_name: 'Birthday',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'zoneinfo',
long_name: 'Time zone',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'locale',
long_name: 'Locale',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'phone_number',
long_name: 'Preferred telephone number',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'phone_number_verified',
long_name: 'True if the phone number has been verified; otherwise false',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'address',
long_name: 'Preferred postal address',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'updated_at',
long_name: 'Time the information was last updated',
ref: '[OpenID Connect Core 1.0 - Section 5.1]',
},
{
name: 'azp',
long_name: 'Authorized party - the party to which the ID Token was issued',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'nonce',
long_name: 'Value used to associate a Client session with an ID Token',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'auth_time',
long_name: 'Time when the authentication occurred',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'at_hash',
long_name: 'Access Token hash value',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'c_hash',
long_name: 'Code hash value',
ref: '[OpenID Connect Core 1.0 - Section 3.3.2.11]',
},
{
name: 'acr',
long_name: 'Authentication Context Class Reference',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'amr',
long_name: 'Authentication Methods References',
ref: '[OpenID Connect Core 1.0 - Section 2]',
},
{
name: 'sub_jwk',
long_name: 'Public key used to check the signature of an ID Token',
ref: '[OpenID Connect Core 1.0 - Section 7.4]',
},
{
name: 'cnf',
long_name: 'Confirmation',
ref: '[RFC7800 - Section 3.1]',
},
{
name: 'sip_from_tag',
long_name: 'SIP From tag header field parameter value',
ref: '[RFC8055][RFC3261]',
},
{
name: 'sip_date',
long_name: 'SIP Date header field value',
ref: '[RFC8055][RFC3261]',
},
{
name: 'sip_callid',
long_name: 'SIP Call-Id header field value',
ref: '[RFC8055][RFC3261]',
},
{
name: 'sip_cseq_num',
long_name: 'SIP CSeq numeric header field parameter value',
ref: '[RFC8055][RFC3261]',
},
{
name: 'sip_via_branch',
long_name: 'SIP Via branch header field parameter value',
ref: '[RFC8055][RFC3261]',
},
{
name: 'orig',
long_name: 'Originating Identity String',
ref: '[RFC8225 - Section 5.2.1]',
},
{
name: 'dest',
long_name: 'Destination Identity String',
ref: '[RFC8225 - Section 5.2.1]',
},
{
name: 'mky',
long_name: 'Media Key Fingerprint String',
ref: '[RFC8225 - Section 5.2.2]',
},
{
name: 'events',
long_name: 'Security Events',
ref: '[RFC8417 - Section 2.2]',
},
{
name: 'toe',
long_name: 'Time of Event',
ref: '[RFC8417 - Section 2.2]',
},
{
name: 'txn',
long_name: 'Transaction Identifier',
ref: '[RFC8417 - Section 2.2]',
},
{
name: 'rph',
long_name: 'Resource Priority Header Authorization',
ref: '[RFC8443 - Section 3]',
},
{
name: 'sid',
long_name: 'Session ID',
ref: '[OpenID Connect Front-Channel Logout 1.0 - Section 3]',
},
{
name: 'vot',
long_name: 'Vector of Trust value',
ref: '[RFC8485]',
},
{
name: 'vtm',
long_name: 'Vector of Trust trustmark URL',
ref: '[RFC8485]',
},
{
name: 'attest',
long_name: 'Attestation level as defined in SHAKEN framework',
ref: '[RFC8588]',
},
{
name: 'origid',
long_name: 'Originating Identifier as defined in SHAKEN framework',
ref: '[RFC8588]',
},
{
name: 'act',
long_name: 'Actor',
ref: '[RFC8693 - Section 4.1]',
},
{
name: 'scope',
long_name: 'Scope Values',
ref: '[RFC8693 - Section 4.2]',
},
{
name: 'client_id',
long_name: 'Client Identifier',
ref: '[RFC8693 - Section 4.3]',
},
{
name: 'may_act',
long_name: 'Authorized Actor - the party that is authorized to become the actor',
ref: '[RFC8693 - Section 4.4]',
},
{
name: 'jcard',
long_name: 'jCard data',
ref: '[RFC8688][RFC7095]',
},
{
name: 'at_use_nbr',
long_name: 'Number of API requests for which the access token can be used',
ref: '[ETSI GS NFV-SEC 022 V2.7.1]',
},
{
name: 'div',
long_name: 'Diverted Target of a Call',
ref: '[RFC8946]',
},
{
name: 'opt',
long_name: 'Original PASSporT (in Full Form)',
ref: '[RFC8946]',
},
{
name: 'vc',
long_name: 'Verifiable Credential as specified in the W3C Recommendation',
ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]',
},
{
name: 'vp',
long_name: 'Verifiable Presentation as specified in the W3C Recommendation',
ref: '[W3C Recommendation Verifiable Credentials Data Model 1.0 - Expressing verifiable information on the Web (19 November 2019) - Section 6.3.1]',
},
{
name: 'sph',
long_name: 'SIP Priority header field',
ref: '[RFC9027]',
},
{
name: 'ace_profile',
long_name: 'The ACE profile a token is supposed to be used with.',
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]',
},
{
name: 'cnonce',
long_name:
'client-nonce. A nonce previously provided to the AS by the RS via the client. Used to verify token freshness when the RS cannot synchronize its clock with the AS.',
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10]',
},
{
name: 'exi',
long_name:
'Expires in. Lifetime of the token in seconds from the time the RS first sees it. Used to implement a weaker from of token expiration for devices that cannot synchronize their internal clocks.',
ref: '[RFC-ietf-ace-oauth-authz-46 - Section 5.10.3]',
},
{
name: 'roles',
long_name: 'Roles',
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
},
{
name: 'groups',
long_name: 'Groups',
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
},
{
name: 'entitlements',
long_name: 'Entitlements',
ref: '[RFC7643 - Section 4.1.2][RFC9068 - Section 2.2.3.1]',
},
{
name: 'token_introspection',
long_name: 'Token introspection response',
ref: '[RFC-ietf-oauth-jwt-introspection-response-12 - Section 5]',
},
];

View File

@@ -1,69 +0,0 @@
<template>
<n-card>
<n-form-item label="JWT to decode" :feedback="validation.message" :validation-status="validation.status">
<n-input v-model:value="raw_jwt" type="textarea" placeholder="Put your token here..." rows="5" />
</n-form-item>
<n-form-item label="Display parsed value?" label-placement="left" :show-feedback="false">
<n-switch v-model:value="showParsedValues" />
</n-form-item>
<n-table>
<tbody>
<td colspan="2" class="table-header"><strong>Header</strong></td>
<tr v-for="[key, value] in Object.entries(decodedJWT.header)" :key="key">
<td class="claims"><claim-vue :claim="key" /></td>
<td>
<span v-if="!showParsedValues">{{ value }}</span>
<value-vue v-else :claim="key" :value="value" />
</td>
</tr>
<td colspan="2" class="table-header"><strong>Payload</strong></td>
<tr v-for="[key, value] in Object.entries(decodedJWT.payload)" :key="key">
<td class="claims"><claim-vue :claim="key" /></td>
<td>
<span v-if="!showParsedValues">{{ value }}</span>
<value-vue v-else :claim="key" :value="value" />
</td>
</tr>
</tbody>
</n-table>
</n-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import jwt_decode from 'jwt-decode';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { safe_jwt_decode } from './jwt-parser.service';
import claimVue from './claim.vue';
import valueVue from './value.vue';
const raw_jwt = ref(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
);
const showParsedValues = ref(true);
const decodedJWT = computed(() => {
return safe_jwt_decode(raw_jwt.value);
});
const validation = useValidation({
source: raw_jwt,
rules: [
{
validator: (value) => value.length > 0 && isNotThrowing(() => jwt_decode(value, { header: true })),
message: 'Invalid JWT',
},
],
});
</script>
<style lang="less" scoped>
.table-header {
text-align: center;
}
.claims {
width: 20%;
}
</style>

View File

@@ -1,24 +0,0 @@
<template>
<n-space>
{{ value.value }}
<em v-if="value.extension">({{ value.extension }})</em>
</n-space>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { parse_claim_value } from './jwt-parser.service';
const props = defineProps({
claim: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
});
const value = computed(() => parse_claim_value(props.claim, props.value));
</script>

29
src/utils/error.test.ts Normal file
View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { getErrorMessageIfThrows } from './error';
describe('error util', () => {
describe('getErrorMessageIfThrows', () => {
it('get an error message if the callback throws, undefined instead', () => {
expect(
getErrorMessageIfThrows(() => {
throw 'message';
}),
).to.equal('message');
expect(
getErrorMessageIfThrows(() => {
throw new Error('message');
}),
).to.equal('message');
expect(
getErrorMessageIfThrows(() => {
throw { message: 'message' };
}),
).to.equal('message');
// eslint-disable-next-line @typescript-eslint/no-empty-function
expect(getErrorMessageIfThrows(() => {})).to.equal(undefined);
});
});
});

24
src/utils/error.ts Normal file
View File

@@ -0,0 +1,24 @@
import _ from 'lodash';
export { getErrorMessageIfThrows };
function getErrorMessageIfThrows(cb: () => unknown) {
try {
cb();
return undefined;
} catch (err) {
if (_.isString(err)) {
return err;
}
if (_.isError(err)) {
return err.message;
}
if (_.isObject(err) && _.has(err, 'message')) {
return (err as { message: string }).message;
}
return 'An error as occurred.';
}
}