From 482ae8b919981d01be7fcef036fb3e4080d6f0e3 Mon Sep 17 00:00:00 2001 From: Paul Fischer Date: Sat, 28 Oct 2023 23:03:15 -0400 Subject: [PATCH] feat(admin api): new /refresh api endpoint (#17) --- .gitignore | 1 - .prettierignore | 10 + .vscode/settings.json | 4 + README.md | 2 +- client/admin/dashboardPage.html | 4 +- client/admin/loginAdminPage.html | 2 +- client/auth/loginPage.html | 6 +- client/auth/profilePage.html | 4 +- client/auth/registrationPage.html | 8 +- client/dist/index.css | 2 +- client/dist/main.css | 298 +++++++++++++----- docker-compose.yml | 9 +- documentation/.vitepress/config.js | 2 + documentation/README.md | 2 +- documentation/guide/gettingstarted.md | 24 +- documentation/guide/integrate.md | 4 +- documentation/guide/launch.md | 2 +- documentation/index.md | 18 +- documentation/package.json | 2 +- documentation/pnpm-lock.yaml | 280 ++++++++-------- documentation/reference/adminapi.md | 62 +++- documentation/reference/authapi.md | 6 +- documentation/reference/tokens.md | 50 +++ package.json | 8 +- plugins/db/db.js | 10 +- plugins/key/admin.key.js | 8 +- plugins/key/server.key.js | 4 +- pnpm-lock.yaml | 223 ++++++------- routes/admin.routes.js | 16 +- routes/auth.routes.js | 6 +- routes/ui.routes.js | 2 +- services/admin/users/create.js | 28 +- services/admin/users/delete.js | 4 +- services/admin/users/list.js | 29 +- services/admin/users/login.js | 47 +-- services/admin/users/logout.js | 15 +- services/admin/users/refresh.js | 74 +++++ services/admin/users/schema/refreshSchema.js | 11 + services/admin/users/update.js | 37 ++- services/auth/login.js | 15 +- services/auth/profile.js | 12 +- services/auth/recovery.js | 10 +- services/auth/refresh.js | 4 +- services/auth/registration.js | 12 +- services/webAuthn/loginOptions.js | 2 +- services/webAuthn/loginVerification.js | 8 +- services/webAuthn/registrationOptions.js | 4 +- services/webAuthn/registrationVerification.js | 6 +- tests/app.test.js | 43 ++- utils/authenticate.js | 7 +- utils/jwt.js | 89 +++++- 51 files changed, 1033 insertions(+), 503 deletions(-) create mode 100644 .prettierignore create mode 100644 .vscode/settings.json create mode 100644 documentation/reference/tokens.md create mode 100644 services/admin/users/refresh.js create mode 100644 services/admin/users/schema/refreshSchema.js diff --git a/.gitignore b/.gitignore index 25d0147..a67340d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ node_modules serverkey adminkey .DS_Store -.vscode .idea \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1695209 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +.github +.vscode +documentation/.vitepress +adminkey +CHANGELOG.md +LICSENSE.md +package-lock.json +package.json +pnpm-lock.yaml +serverkey \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b92b3ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + } \ No newline at end of file diff --git a/README.md b/README.md index db9cdb7..7183c16 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ With AuthC you can: - Manage Users via a self-service Dashboard and confidently store and own your web application's user accounts, on your terms. - Help users create accounts and login with a passkey on their mobile device (aka passwordless) or by using traditional username/passwords instead. - | Login Screen | Registration Screen | | :----------------------------------: | :----------------------------------------: | | ![Login](./.github/public/login.png) | ![Register](./.github/public/register.png) | @@ -76,6 +75,7 @@ Start the server (with the default config): ```bash $ docker run -it -p 3002:3002 --name AuthCompanion ghcr.io/authcompanion/authcompanion2:main ``` + Also available is the [docker-compose.yml](https://github.com/authcompanion/authcompanion2/blob/main/docker-compose.yml) ### Configure AuthCompanion diff --git a/client/admin/dashboardPage.html b/client/admin/dashboardPage.html index 48a0f8f..f92384f 100644 --- a/client/admin/dashboardPage.html +++ b/client/admin/dashboardPage.html @@ -909,7 +909,7 @@

"Content-type": "application/json", Authorization: `Bearer ${token}`, }, - } + }, ); const data = await response.json(); // If the response is not ok, throw an error @@ -922,7 +922,7 @@

// Call the showNotificationMessage function to show a success message showNotificationMessage( - `Found ${users.value.data.length} user(s) matching the search.` + `Found ${users.value.data.length} user(s) matching the search.`, ); } catch (e) { console.log("Search User Failed"); diff --git a/client/admin/loginAdminPage.html b/client/admin/loginAdminPage.html index cc31a53..9be49c8 100644 --- a/client/admin/loginAdminPage.html +++ b/client/admin/loginAdminPage.html @@ -232,7 +232,7 @@ if (response.ok) { window.localStorage.setItem( "ACCESS_TOKEN", - resbody.data.attributes.access_token + resbody.data.attributes.access_token, ); window.location.href = appOrigin; diff --git a/client/auth/loginPage.html b/client/auth/loginPage.html index 1345a21..e462da5 100644 --- a/client/auth/loginPage.html +++ b/client/auth/loginPage.html @@ -270,7 +270,7 @@ if (response.ok) { window.localStorage.setItem( "ACCESS_TOKEN", - resbody.data.attributes.access_token + resbody.data.attributes.access_token, ); window.location.href = appOrigin; @@ -318,7 +318,7 @@ "content-type": "application/json", "x-authc-app-challenge": opts.challenge, }, - } + }, ); const reply = await verificationResponse.json(); const appOrigin = @@ -326,7 +326,7 @@ window.localStorage.setItem( "ACCESS_TOKEN", - reply.data.attributes.access_token + reply.data.attributes.access_token, ); showError.value = false; window.location.href = appOrigin; diff --git a/client/auth/profilePage.html b/client/auth/profilePage.html index 94c268a..09346ce 100644 --- a/client/auth/profilePage.html +++ b/client/auth/profilePage.html @@ -343,7 +343,7 @@ const refreshBody = await refreshResponse.json(); window.localStorage.setItem( "ACCESS_TOKEN", - refreshBody.data.attributes.access_token + refreshBody.data.attributes.access_token, ); const updatedResponse = await updateUser(); @@ -357,7 +357,7 @@ } else if (response.ok) { window.localStorage.setItem( "ACCESS_TOKEN", - webserviceResponse.data.attributes.access_token + webserviceResponse.data.attributes.access_token, ); showError.value = true; errorTitle.value = "Success"; diff --git a/client/auth/registrationPage.html b/client/auth/registrationPage.html index e075635..00f9f82 100644 --- a/client/auth/registrationPage.html +++ b/client/auth/registrationPage.html @@ -323,7 +323,7 @@ if (response.ok) { window.localStorage.setItem( "ACCESS_TOKEN", - resbody.data.attributes.access_token + resbody.data.attributes.access_token, ); showError.value = false; @@ -354,7 +354,7 @@ headers: { "content-type": "application/json", }, - } + }, ); if (optionsResponse.ok) { @@ -371,7 +371,7 @@ "content-type": "application/json", "x-authc-app-userid": opts.user.id, }, - } + }, ); const reply = await verificationResponse.json(); const appOrigin = @@ -379,7 +379,7 @@ window.localStorage.setItem( "ACCESS_TOKEN", - reply.data.attributes.access_token + reply.data.attributes.access_token, ); showError.value = false; showError.value = false; diff --git a/client/dist/index.css b/client/dist/index.css index bd6213e..b5c61c9 100644 --- a/client/dist/index.css +++ b/client/dist/index.css @@ -1,3 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/client/dist/main.css b/client/dist/main.css index 2c7bac9..eb6ab5e 100644 --- a/client/dist/main.css +++ b/client/dist/main.css @@ -22,7 +22,7 @@ ::before, ::after { - --tw-content: ''; + --tw-content: ""; } /* @@ -42,9 +42,23 @@ html { -moz-tab-size: 4; /* 3 */ -o-tab-size: 4; - tab-size: 4; + tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ @@ -85,7 +99,7 @@ Add the correct text decoration in Chrome, Edge, and Safari. abbr:where([title]) { -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; + text-decoration: underline dotted; } /* @@ -129,7 +143,8 @@ code, kbd, samp, pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; /* 1 */ font-size: 1em; /* 2 */ @@ -224,9 +239,9 @@ select { */ button, -[type='button'], -[type='reset'], -[type='submit'] { +[type="button"], +[type="reset"], +[type="submit"] { -webkit-appearance: button; /* 1 */ background-color: transparent; @@ -273,7 +288,7 @@ Correct the cursor style of increment and decrement buttons in Safari. 2. Correct the outline style in Safari. */ -[type='search'] { +[type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; @@ -366,7 +381,8 @@ textarea { 2. Set the default placeholder color to the user's configured gray 400 color. */ -input::-moz-placeholder, textarea::-moz-placeholder { +input::-moz-placeholder, +textarea::-moz-placeholder { opacity: 1; /* 1 */ color: #9ca3af; @@ -434,10 +450,25 @@ video { display: none; } -[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { +[type="text"], +input:where(:not([type])), +[type="email"], +[type="url"], +[type="password"], +[type="number"], +[type="date"], +[type="datetime-local"], +[type="month"], +[type="search"], +[type="tel"], +[type="time"], +[type="week"], +[multiple], +textarea, +select { -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + -moz-appearance: none; + appearance: none; background-color: #fff; border-color: #6b7280; border-width: 1px; @@ -451,25 +482,45 @@ video { --tw-shadow: 0 0 #0000; } -[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { +[type="text"]:focus, +input:where(:not([type])):focus, +[type="email"]:focus, +[type="url"]:focus, +[type="password"]:focus, +[type="number"]:focus, +[type="date"]:focus, +[type="datetime-local"]:focus, +[type="month"]:focus, +[type="search"]:focus, +[type="tel"]:focus, +[type="time"]:focus, +[type="week"]:focus, +[multiple]:focus, +textarea:focus, +select:focus { outline: 2px solid transparent; outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow); border-color: #2563eb; } -input::-moz-placeholder, textarea::-moz-placeholder { +input::-moz-placeholder, +textarea::-moz-placeholder { color: #6b7280; opacity: 1; } -input::placeholder,textarea::placeholder { +input::placeholder, +textarea::placeholder { color: #6b7280; opacity: 1; } @@ -487,7 +538,15 @@ input::placeholder,textarea::placeholder { display: inline-flex; } -::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { +::-webkit-datetime-edit, +::-webkit-datetime-edit-year-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-minute-field, +::-webkit-datetime-edit-second-field, +::-webkit-datetime-edit-millisecond-field, +::-webkit-datetime-edit-meridiem-field { padding-top: 0; padding-bottom: 0; } @@ -499,32 +558,34 @@ select { background-size: 1.5em 1.5em; padding-right: 2.5rem; -webkit-print-color-adjust: exact; - print-color-adjust: exact; + print-color-adjust: exact; } -[multiple],[size]:where(select:not([size="1"])) { +[multiple], +[size]:where(select:not([size="1"])) { background-image: initial; background-position: initial; background-repeat: unset; background-size: initial; padding-right: 0.75rem; -webkit-print-color-adjust: unset; - print-color-adjust: unset; + print-color-adjust: unset; } -[type='checkbox'],[type='radio'] { +[type="checkbox"], +[type="radio"] { -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + -moz-appearance: none; + appearance: none; padding: 0; -webkit-print-color-adjust: exact; - print-color-adjust: exact; + print-color-adjust: exact; display: inline-block; vertical-align: middle; background-origin: border-box; -webkit-user-select: none; - -moz-user-select: none; - user-select: none; + -moz-user-select: none; + user-select: none; flex-shrink: 0; height: 1rem; width: 1rem; @@ -535,27 +596,32 @@ select { --tw-shadow: 0 0 #0000; } -[type='checkbox'] { +[type="checkbox"] { border-radius: 0px; } -[type='radio'] { +[type="radio"] { border-radius: 100%; } -[type='checkbox']:focus,[type='radio']:focus { +[type="checkbox"]:focus, +[type="radio"]:focus { outline: 2px solid transparent; outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); --tw-ring-offset-width: 2px; --tw-ring-offset-color: #fff; --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow); } -[type='checkbox']:checked,[type='radio']:checked { +[type="checkbox"]:checked, +[type="radio"]:checked { border-color: transparent; background-color: currentColor; background-size: 100% 100%; @@ -563,20 +629,23 @@ select { background-repeat: no-repeat; } -[type='checkbox']:checked { +[type="checkbox"]:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); } -[type='radio']:checked { +[type="radio"]:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); } -[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { +[type="checkbox"]:checked:hover, +[type="checkbox"]:checked:focus, +[type="radio"]:checked:hover, +[type="radio"]:checked:focus { border-color: transparent; background-color: currentColor; } -[type='checkbox']:indeterminate { +[type="checkbox"]:indeterminate { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); border-color: transparent; background-color: currentColor; @@ -585,12 +654,13 @@ select { background-repeat: no-repeat; } -[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { +[type="checkbox"]:indeterminate:hover, +[type="checkbox"]:indeterminate:focus { border-color: transparent; background-color: currentColor; } -[type='file'] { +[type="file"] { background: unset; border-color: inherit; border-width: 0; @@ -600,12 +670,14 @@ select { line-height: inherit; } -[type='file']:focus { +[type="file"]:focus { outline: 1px solid ButtonText; outline: 1px auto -webkit-focus-ring-color; } -*, ::before, ::after { +*, +::before, +::after { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; @@ -838,7 +910,7 @@ select { .ms-4 { -webkit-margin-start: 1rem; - margin-inline-start: 1rem; + margin-inline-start: 1rem; } .mt-1 { @@ -1032,26 +1104,36 @@ select { .-translate-y-1\/2 { --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .translate-y-0 { --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .translate-y-2 { --tw-translate-y: 0.5rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .-rotate-90 { --tw-rotate: -90deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .cursor-pointer { @@ -1361,7 +1443,7 @@ select { .pe-12 { -webkit-padding-end: 3rem; - padding-inline-end: 3rem; + padding-inline-end: 3rem; } .pl-10 { @@ -1548,32 +1630,44 @@ select { .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), + 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .shadow-lg { - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .shadow-sm { --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .shadow-xl { - --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), + 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), + 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .ring-1 { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow, 0 0 #0000); } .ring-black { @@ -1586,9 +1680,34 @@ select { } .transition { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-property: + color, + background-color, + border-color, + text-decoration-color, + fill, + stroke, + opacity, + box-shadow, + transform, + filter, + -webkit-backdrop-filter; + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter; + transition-property: + color, + background-color, + border-color, + text-decoration-color, + fill, + stroke, + opacity, + box-shadow, + transform, + filter, + backdrop-filter, + -webkit-backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } @@ -1693,15 +1812,21 @@ select { } .focus\:ring:focus { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow, 0 0 #0000); } .focus\:ring-2:focus { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow, 0 0 #0000); } .focus\:ring-blue-500:focus { @@ -1736,7 +1861,8 @@ select { .disabled\:shadow-none:disabled { --tw-shadow: 0 0 #0000; --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .group:hover .group-hover\:text-indigo-400 { @@ -1750,7 +1876,9 @@ select { :is([dir="rtl"] .rtl\:rotate-180) { --tw-rotate: 180deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @media (min-width: 640px) { @@ -1769,17 +1897,23 @@ select { .sm\:translate-x-0 { --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .sm\:translate-x-2 { --tw-translate-x: 0.5rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .sm\:translate-y-0 { --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) + rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) + scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } .sm\:flex-row { @@ -1856,7 +1990,7 @@ select { .\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button { -webkit-appearance: none; - appearance: none; + appearance: none; } .\[\&\:\:-webkit-outer-spin-button\]\:m-0::-webkit-outer-spin-button { @@ -1865,5 +1999,5 @@ select { .\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button { -webkit-appearance: none; - appearance: none; -} \ No newline at end of file + appearance: none; +} diff --git a/docker-compose.yml b/docker-compose.yml index 1bcfe6b..ebd4471 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: authcompanion: @@ -8,11 +8,10 @@ services: - DB_PATH=./authdata/authcompanion_users.db - ADMIN_KEY_PATH=./authdata/adminkey - KEY_PATH=./authdata/serverkey - ports: - - "3002:3002" + ports: + - "3002:3002" volumes: - authdata:/home/nodejs/app/authdata - volumes: - authdata: \ No newline at end of file + authdata: diff --git a/documentation/.vitepress/config.js b/documentation/.vitepress/config.js index ace8d4f..050b6fe 100644 --- a/documentation/.vitepress/config.js +++ b/documentation/.vitepress/config.js @@ -44,6 +44,8 @@ export default { items: [ { text: "Authentication API", link: "/reference/authapi" }, { text: "Admin API", link: "/reference/adminapi" }, + { text: "Tokens & Claims", link: "/reference/tokens" }, + ], }, ], diff --git a/documentation/README.md b/documentation/README.md index 4cec6e4..b8a24c1 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1 +1 @@ -### Documentation powering https://docs.authcompanion.com/ \ No newline at end of file +### Documentation powering https://docs.authcompanion.com/ diff --git a/documentation/guide/gettingstarted.md b/documentation/guide/gettingstarted.md index 77c8d59..587af1e 100644 --- a/documentation/guide/gettingstarted.md +++ b/documentation/guide/gettingstarted.md @@ -19,7 +19,7 @@ Pre-requirement: - Make sure you have [Node.js](http://nodejs.org) installed **latest TLS version - v18 ** -Let's install the application's packages. +Let's install the application's packages. ```bash $ npm install @@ -42,11 +42,11 @@ $ cp env.example .env Then restart the server to apply your new settings. -## With Docker +## Docker Make sure to have the [respository cloned](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) -as outlined in the steps above. +as outlined in the steps above. Then from the main directory, build the AuthC server image: @@ -63,7 +63,21 @@ authc_server ``` If you have your own configuration file you can pass it into your docker command -with: `--env-file .env \` but this is optional. +with: `--env-file .env \` but this is optional. + +### AuthC Container Image + +Container images are published for both the main branch and for the latest tagged version. + +Please see the container registry [here](https://github.com/authcompanion/authcompanion2/pkgs/container/authcompanion2) + +Start the server (with the default config): + +```bash +$ docker run -it -p 3002:3002 --name AuthCompanion ghcr.io/authcompanion/authcompanion2:main +``` + +Also available is the [docker-compose.yml](https://github.com/authcompanion/authcompanion2/blob/main/docker-compose.yml) ## Using AuthCompanion @@ -71,7 +85,7 @@ When the server is properly configured and running there are two main entries into AuthCompanion. 🖥️ The web forms, available to your application users. The login starts here: -`http://localhost:3002/v1/web/login`. +`http://localhost:3002/v1/web/login`. Or check out the registration page here: `http://localhost:3002/v1/web/register` diff --git a/documentation/guide/integrate.md b/documentation/guide/integrate.md index daaf7f9..141b882 100644 --- a/documentation/guide/integrate.md +++ b/documentation/guide/integrate.md @@ -54,7 +54,7 @@ export async function importKey() { hash: "SHA-256", }, true, - ["sign", "verify"] + ["sign", "verify"], ); return key; } catch (error) { @@ -195,7 +195,7 @@ async function refreshAccessToken() { if (refreshResponse.ok) { localStorage.setItem( "ACCESS_TOKEN", - refreshBody.data.attributes.access_token + refreshBody.data.attributes.access_token, ); } diff --git a/documentation/guide/launch.md b/documentation/guide/launch.md index aa56851..10dc5b9 100644 --- a/documentation/guide/launch.md +++ b/documentation/guide/launch.md @@ -193,4 +193,4 @@ For an example `fly.toml` file, check out AuthCompanion's configuration used to AuthCompanion uses fly.io to host the demo site. If you get lost and want to see an example of a working configuration to compare with your setup - see the `fly.toml` file here: -https://github.com/authcompanion/authcompanion2/blob/main/fly.toml \ No newline at end of file +https://github.com/authcompanion/authcompanion2/blob/main/fly.toml diff --git a/documentation/index.md b/documentation/index.md index 45ddb5b..51586fc 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -46,21 +46,21 @@ With AuthC you can: ## Features - **Web Forms for User Authentication:** Use pre-built and customizable web - forms for your application users to: log in with their credentials, - register an account, update their profile, and issue forgotten passwords. + forms for your application users to: log in with their credentials, + register an account, update their profile, and issue forgotten passwords. - **Manage User Profiles and JWTs:** Update the password and profile - information of your users - all account information is stored in a SQLite - database. Easily manage the life-cycle of your user's JWT used for - authentication. + information of your users - all account information is stored in a SQLite + database. Easily manage the life-cycle of your user's JWT used for + authentication. - **User Account Recovery:** Restore a user's access to their account using - the **Forgot Password** flow which sends a special link via email for - helping users quickly recover their account. + the **Forgot Password** flow which sends a special link via email for + helping users quickly recover their account. - **Extensible Platform:** AuthC supports a - [plugin system](https://www.fastify.io/docs/latest/Reference/Plugins/) for - easily adding new functionality to cover more of your authentication needs. + [plugin system](https://www.fastify.io/docs/latest/Reference/Plugins/) for + easily adding new functionality to cover more of your authentication needs. - **Passwordless Flow:** Streamline user Login and Registration without passwords with a user's computer or mobile phone with passkey. diff --git a/documentation/package.json b/documentation/package.json index b623f19..55d51bd 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -12,6 +12,6 @@ "author": "Paul Fischer", "license": "MIT", "devDependencies": { - "vitepress": "1.0.0-rc.13" + "vitepress": "1.0.0-rc.23" } } \ No newline at end of file diff --git a/documentation/pnpm-lock.yaml b/documentation/pnpm-lock.yaml index 0651de5..226746e 100644 --- a/documentation/pnpm-lock.yaml +++ b/documentation/pnpm-lock.yaml @@ -6,15 +6,15 @@ settings: devDependencies: vitepress: - specifier: 1.0.0-rc.13 - version: 1.0.0-rc.13(@algolia/client-search@4.20.0)(search-insights@2.8.2) + specifier: 1.0.0-rc.23 + version: 1.0.0-rc.23(@algolia/client-search@4.20.0)(search-insights@2.9.0) packages: - /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2): + /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0): resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2) + '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0) '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) transitivePeerDependencies: - '@algolia/client-search' @@ -22,13 +22,13 @@ packages: - search-insights dev: true - /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2): + /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0): resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} peerDependencies: search-insights: '>= 1 < 3' dependencies: '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) - search-insights: 2.8.2 + search-insights: 2.9.0 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch @@ -150,25 +150,25 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-identifier@7.22.19: - resolution: {integrity: sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==} + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} dev: true - /@babel/parser@7.22.16: - resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==} + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.22.19 + '@babel/types': 7.23.0 dev: true - /@babel/types@7.22.19: - resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==} + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.22.5 - '@babel/helper-validator-identifier': 7.22.19 + '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 dev: true @@ -176,11 +176,11 @@ packages: resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==} dev: true - /@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2): + /@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0): resolution: {integrity: sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==} dependencies: - '@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2) - preact: 10.17.1 + '@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0) + preact: 10.18.1 transitivePeerDependencies: - '@algolia/client-search' - '@types/react' @@ -189,7 +189,7 @@ packages: - search-insights dev: true - /@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2): + /@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0): resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' @@ -206,11 +206,11 @@ packages: search-insights: optional: true dependencies: - '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2) + '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0) '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) '@docsearch/css': 3.5.2 algoliasearch: 4.20.0 - search-insights: 2.8.2 + search-insights: 2.9.0 transitivePeerDependencies: - '@algolia/client-search' dev: true @@ -417,111 +417,126 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@types/web-bluetooth@0.0.17: - resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} + /@types/linkify-it@3.0.4: + resolution: {integrity: sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ==} dev: true - /@vue/compiler-core@3.3.4: - resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + /@types/markdown-it@13.0.5: + resolution: {integrity: sha512-QhJP7hkq3FCrFNx0szMNCT/79CXfcEgUIA3jc5GBfeXqoKsk3R8JZm2wRXJ2DiyjbPE4VMFOSDemLFcUTZmHEQ==} dependencies: - '@babel/parser': 7.22.16 - '@vue/shared': 3.3.4 + '@types/linkify-it': 3.0.4 + '@types/mdurl': 1.0.4 + dev: true + + /@types/mdurl@1.0.4: + resolution: {integrity: sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A==} + dev: true + + /@types/web-bluetooth@0.0.18: + resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==} + dev: true + + /@vue/compiler-core@3.3.6: + resolution: {integrity: sha512-2JNjemwaNwf+MkkatATVZi7oAH1Hx0B04DdPH3ZoZ8vKC1xZVP7nl4HIsk8XYd3r+/52sqqoz9TWzYc3yE9dqA==} + dependencies: + '@babel/parser': 7.23.0 + '@vue/shared': 3.3.6 estree-walker: 2.0.2 source-map-js: 1.0.2 dev: true - /@vue/compiler-dom@3.3.4: - resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + /@vue/compiler-dom@3.3.6: + resolution: {integrity: sha512-1MxXcJYMHiTPexjLAJUkNs/Tw2eDf2tY3a0rL+LfuWyiKN2s6jvSwywH3PWD8bKICjfebX3GWx2Os8jkRDq3Ng==} dependencies: - '@vue/compiler-core': 3.3.4 - '@vue/shared': 3.3.4 + '@vue/compiler-core': 3.3.6 + '@vue/shared': 3.3.6 dev: true - /@vue/compiler-sfc@3.3.4: - resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} + /@vue/compiler-sfc@3.3.6: + resolution: {integrity: sha512-/Kms6du2h1VrXFreuZmlvQej8B1zenBqIohP0690IUBkJjsFvJxY0crcvVRJ0UhMgSR9dewB+khdR1DfbpArJA==} dependencies: - '@babel/parser': 7.22.16 - '@vue/compiler-core': 3.3.4 - '@vue/compiler-dom': 3.3.4 - '@vue/compiler-ssr': 3.3.4 - '@vue/reactivity-transform': 3.3.4 - '@vue/shared': 3.3.4 + '@babel/parser': 7.23.0 + '@vue/compiler-core': 3.3.6 + '@vue/compiler-dom': 3.3.6 + '@vue/compiler-ssr': 3.3.6 + '@vue/reactivity-transform': 3.3.6 + '@vue/shared': 3.3.6 estree-walker: 2.0.2 - magic-string: 0.30.3 - postcss: 8.4.29 + magic-string: 0.30.5 + postcss: 8.4.31 source-map-js: 1.0.2 dev: true - /@vue/compiler-ssr@3.3.4: - resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} + /@vue/compiler-ssr@3.3.6: + resolution: {integrity: sha512-QTIHAfDCHhjXlYGkUg5KH7YwYtdUM1vcFl/FxFDlD6d0nXAmnjizka3HITp8DGudzHndv2PjKVS44vqqy0vP4w==} dependencies: - '@vue/compiler-dom': 3.3.4 - '@vue/shared': 3.3.4 + '@vue/compiler-dom': 3.3.6 + '@vue/shared': 3.3.6 dev: true - /@vue/devtools-api@6.5.0: - resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} + /@vue/devtools-api@6.5.1: + resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} dev: true - /@vue/reactivity-transform@3.3.4: - resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} + /@vue/reactivity-transform@3.3.6: + resolution: {integrity: sha512-RlJl4dHfeO7EuzU1iJOsrlqWyJfHTkJbvYz/IOJWqu8dlCNWtxWX377WI0VsbAgBizjwD+3ZjdnvSyyFW1YVng==} dependencies: - '@babel/parser': 7.22.16 - '@vue/compiler-core': 3.3.4 - '@vue/shared': 3.3.4 + '@babel/parser': 7.23.0 + '@vue/compiler-core': 3.3.6 + '@vue/shared': 3.3.6 estree-walker: 2.0.2 - magic-string: 0.30.3 + magic-string: 0.30.5 dev: true - /@vue/reactivity@3.3.4: - resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} + /@vue/reactivity@3.3.6: + resolution: {integrity: sha512-gtChAumfQz5lSy5jZXfyXbKrIYPf9XEOrIr6rxwVyeWVjFhJwmwPLtV6Yis+M9onzX++I5AVE9j+iPH60U+B8Q==} dependencies: - '@vue/shared': 3.3.4 + '@vue/shared': 3.3.6 dev: true - /@vue/runtime-core@3.3.4: - resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} + /@vue/runtime-core@3.3.6: + resolution: {integrity: sha512-qp7HTP1iw1UW2ZGJ8L3zpqlngrBKvLsDAcq5lA6JvEXHmpoEmjKju7ahM9W2p/h51h0OT5F2fGlP/gMhHOmbUA==} dependencies: - '@vue/reactivity': 3.3.4 - '@vue/shared': 3.3.4 + '@vue/reactivity': 3.3.6 + '@vue/shared': 3.3.6 dev: true - /@vue/runtime-dom@3.3.4: - resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} + /@vue/runtime-dom@3.3.6: + resolution: {integrity: sha512-AoX3Cp8NqMXjLbIG9YR6n/pPLWE9TiDdk6wTJHFnl2GpHzDFH1HLBC9wlqqQ7RlnvN3bVLpzPGAAH00SAtOxHg==} dependencies: - '@vue/runtime-core': 3.3.4 - '@vue/shared': 3.3.4 + '@vue/runtime-core': 3.3.6 + '@vue/shared': 3.3.6 csstype: 3.1.2 dev: true - /@vue/server-renderer@3.3.4(vue@3.3.4): - resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} + /@vue/server-renderer@3.3.6(vue@3.3.6): + resolution: {integrity: sha512-kgLoN43W4ERdZ6dpyy+gnk2ZHtcOaIr5Uc/WUP5DRwutgvluzu2pudsZGoD2b7AEJHByUVMa9k6Sho5lLRCykw==} peerDependencies: - vue: 3.3.4 + vue: 3.3.6 dependencies: - '@vue/compiler-ssr': 3.3.4 - '@vue/shared': 3.3.4 - vue: 3.3.4 + '@vue/compiler-ssr': 3.3.6 + '@vue/shared': 3.3.6 + vue: 3.3.6 dev: true - /@vue/shared@3.3.4: - resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} + /@vue/shared@3.3.6: + resolution: {integrity: sha512-Xno5pEqg8SVhomD0kTSmfh30ZEmV/+jZtyh39q6QflrjdJCXah5lrnOLi9KB6a5k5aAHXMXjoMnxlzUkCNfWLQ==} dev: true - /@vueuse/core@10.4.1(vue@3.3.4): - resolution: {integrity: sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg==} + /@vueuse/core@10.5.0(vue@3.3.6): + resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==} dependencies: - '@types/web-bluetooth': 0.0.17 - '@vueuse/metadata': 10.4.1 - '@vueuse/shared': 10.4.1(vue@3.3.4) - vue-demi: 0.14.6(vue@3.3.4) + '@types/web-bluetooth': 0.0.18 + '@vueuse/metadata': 10.5.0 + '@vueuse/shared': 10.5.0(vue@3.3.6) + vue-demi: 0.14.6(vue@3.3.6) transitivePeerDependencies: - '@vue/composition-api' - vue dev: true - /@vueuse/integrations@10.4.1(focus-trap@7.5.2)(vue@3.3.4): - resolution: {integrity: sha512-uRBPyG5Lxoh1A/J+boiioPT3ELEAPEo4t8W6Mr4yTKIQBeW/FcbsotZNPr4k9uz+3QEksMmflWloS9wCnypM7g==} + /@vueuse/integrations@10.5.0(focus-trap@7.5.4)(vue@3.3.6): + resolution: {integrity: sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==} peerDependencies: async-validator: '*' axios: '*' @@ -561,23 +576,23 @@ packages: universal-cookie: optional: true dependencies: - '@vueuse/core': 10.4.1(vue@3.3.4) - '@vueuse/shared': 10.4.1(vue@3.3.4) - focus-trap: 7.5.2 - vue-demi: 0.14.6(vue@3.3.4) + '@vueuse/core': 10.5.0(vue@3.3.6) + '@vueuse/shared': 10.5.0(vue@3.3.6) + focus-trap: 7.5.4 + vue-demi: 0.14.6(vue@3.3.6) transitivePeerDependencies: - '@vue/composition-api' - vue dev: true - /@vueuse/metadata@10.4.1: - resolution: {integrity: sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg==} + /@vueuse/metadata@10.5.0: + resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==} dev: true - /@vueuse/shared@10.4.1(vue@3.3.4): - resolution: {integrity: sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==} + /@vueuse/shared@10.5.0(vue@3.3.6): + resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} dependencies: - vue-demi: 0.14.6(vue@3.3.4) + vue-demi: 0.14.6(vue@3.3.6) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -644,8 +659,8 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true - /focus-trap@7.5.2: - resolution: {integrity: sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==} + /focus-trap@7.5.4: + resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} dependencies: tabbable: 6.2.0 dev: true @@ -662,8 +677,8 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true - /magic-string@0.30.3: - resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -687,8 +702,8 @@ packages: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true - /postcss@8.4.29: - resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 @@ -696,24 +711,24 @@ packages: source-map-js: 1.0.2 dev: true - /preact@10.17.1: - resolution: {integrity: sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==} + /preact@10.18.1: + resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==} dev: true - /rollup@3.29.1: - resolution: {integrity: sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==} + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: fsevents: 2.3.3 dev: true - /search-insights@2.8.2: - resolution: {integrity: sha512-PxA9M5Q2bpBelVvJ3oDZR8nuY00Z6qwOxL53wNpgzV28M/D6u9WUbImDckjLSILBF8F1hn/mgyuUaOPtjow4Qw==} + /search-insights@2.9.0: + resolution: {integrity: sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==} dev: true - /shiki@0.14.4: - resolution: {integrity: sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==} + /shiki@0.14.5: + resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} dependencies: ansi-sequence-parser: 1.1.1 jsonc-parser: 3.2.0 @@ -735,8 +750,8 @@ packages: engines: {node: '>=4'} dev: true - /vite@4.4.9: - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} + /vite@4.5.0: + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -764,27 +779,36 @@ packages: optional: true dependencies: esbuild: 0.18.20 - postcss: 8.4.29 - rollup: 3.29.1 + postcss: 8.4.31 + rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 dev: true - /vitepress@1.0.0-rc.13(@algolia/client-search@4.20.0)(search-insights@2.8.2): - resolution: {integrity: sha512-TnVydQOZE38rtXu9gHCb7EGdN03jTcmYkDdhCqox6+pfKYgiyfm1qk2Uy8BZatnM9wXpa64f+T5p30R8P/9Z+A==} + /vitepress@1.0.0-rc.23(@algolia/client-search@4.20.0)(search-insights@2.9.0): + resolution: {integrity: sha512-0YoBt8aFgbRt2JtYaCeTqq4W21q5lbGso+g1ZwkYYS35ExJxORssRJunhFuUcby8QeN4BP/88QDgsVSIVLAfXQ==} hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4.3.2 + postcss: ^8.4.31 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true dependencies: '@docsearch/css': 3.5.2 - '@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2) - '@vue/devtools-api': 6.5.0 - '@vueuse/core': 10.4.1(vue@3.3.4) - '@vueuse/integrations': 10.4.1(focus-trap@7.5.2)(vue@3.3.4) - focus-trap: 7.5.2 + '@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0) + '@types/markdown-it': 13.0.5 + '@vue/devtools-api': 6.5.1 + '@vueuse/core': 10.5.0(vue@3.3.6) + '@vueuse/integrations': 10.5.0(focus-trap@7.5.4)(vue@3.3.6) + focus-trap: 7.5.4 mark.js: 8.11.1 minisearch: 6.1.0 - shiki: 0.14.4 - vite: 4.4.9 - vue: 3.3.4 + shiki: 0.14.5 + vite: 4.5.0 + vue: 3.3.6 transitivePeerDependencies: - '@algolia/client-search' - '@types/node' @@ -809,6 +833,7 @@ packages: - stylus - sugarss - terser + - typescript - universal-cookie dev: true @@ -820,7 +845,7 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: true - /vue-demi@0.14.6(vue@3.3.4): + /vue-demi@0.14.6(vue@3.3.6): resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} engines: {node: '>=12'} hasBin: true @@ -832,15 +857,20 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.3.4 + vue: 3.3.6 dev: true - /vue@3.3.4: - resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} + /vue@3.3.6: + resolution: {integrity: sha512-jJIDETeWJnoY+gfn4ZtMPMS5KtbP4ax+CT4dcQFhTnWEk8xMupFyQ0JxL28nvT/M4+p4a0ptxaV2WY0LiIxvRg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true dependencies: - '@vue/compiler-dom': 3.3.4 - '@vue/compiler-sfc': 3.3.4 - '@vue/runtime-dom': 3.3.4 - '@vue/server-renderer': 3.3.4(vue@3.3.4) - '@vue/shared': 3.3.4 + '@vue/compiler-dom': 3.3.6 + '@vue/compiler-sfc': 3.3.6 + '@vue/runtime-dom': 3.3.6 + '@vue/server-renderer': 3.3.6(vue@3.3.6) + '@vue/shared': 3.3.6 dev: true diff --git a/documentation/reference/adminapi.md b/documentation/reference/adminapi.md index 46bdd02..c790d54 100644 --- a/documentation/reference/adminapi.md +++ b/documentation/reference/adminapi.md @@ -1,12 +1,11 @@ # Admin API -The RESTful Admin API helps you to manage your Authcompanion Users for administrative purposes. And can be used to power an Authcompanion Admin Panel. +The RESTful Admin API helps you to programatically manage your Authcompanion Users for administrative purposes. +The Admin APIs power AuthCompanions self-service Admin Dashboard. ## Admin Access Token (Bearer Token) -All Admin API requests require a Bearer Token in the request's header. This token is generated when you call the `admin/login` endpoint (described below). The token is a JWT (JSON Web Token) that contains the user's ID, name, email, and scope. The scope is always `admin` for the admin user. - -The JWT itself has a expiration time of 2 hours. After that time, you will need to generate a new token by calling the `admin/login` endpoint again. +All Admin API requests require a Bearer Token in the request's header. This token is generated when you call the `admin/login` endpoint (described below) and allows access to the Admin APIs. ## Server URL @@ -18,7 +17,7 @@ Returns Content-Type: application/json ### admin/login -Description: Trades the admin credentials for an admin access token used to access the Admin API. See more information about the admin credentials the administer section of the documentation at [Administer](/guide/administer.md). Only Admin tokens can access the Admin API. +Description: Trades the admin credentials for an admin access token used to access the various Admin API endpoints. See more information about the admin credentials in the administer section of the documentation at [Administer](/guide/administer.md). **POST** Request Body: @@ -40,13 +39,14 @@ Response: { "data": { "type": "users", - "id": "01f900ba-2c5e-4e0b-84e0-12355d731de4", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Admin", "email": "admin@localhost", - "created": "2023-03-12T17:33:48.636Z", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiIwMWY5MDBiYS0yYzVlLTRlMGItODRlMC0xMjM1NWQ3MzFkZTQiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInNjb3BlIjoiYWRtaW4iLCJpYXQiOjE2Nzg2Nzc2NzksImV4cCI6MTY3ODY4NDg3OX0.d-vycADtZehogLeYSdrs0mQ_4YhHwNBuiAS7UaD1ozs", - "access_token_expiry": 1678684879 + "created": "2023-10-12T16:10:17.709Z", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJ3ejVkY3RvMmJqNW9lb2Q2N2VoajN0dmwiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInVzZXJGaW5nZXJwcmludCI6IiRhcmdvbjJpZCR2PTE5JG09NjQwMDAsdD0zLHA9MSRpWlpOT2MwbitJUzhnUUwxYjJ2Wld3JEJyZERpazlZR0VOTjdqZEt3ckRnMGxzRXMrVnY0OWt3c0pKOVQrTHBxSHMiLCJzY29wZSI6ImFkbWluIiwiaWF0IjoxNjk4MDIzODA3LCJleHAiOjE2OTgwMjc0MDd9.pGVrJc8rrFFyMsaE9ubUoXl_3vfNOXTs6x6q8N6ETCU", + "access_token_expiry": 1698027407, + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJ3ejVkY3RvMmJqNW9lb2Q2N2VoajN0dmwiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInVzZXJGaW5nZXJwcmludCI6bnVsbCwic2NvcGUiOiJhZG1pbiIsImlhdCI6MTY5ODAyMzgwNywiZXhwIjoxNjk4NjI4NjA3LCJqdGkiOiI2YTkxOGQ5NC1jNGVmLTRjOWItODFkMy05Yjg1OGFiOWU3YWEifQ.720K2t94iC_dTwGCd2IFV1WpHY9Jsmg_iK2hh2wj5h4" } } } @@ -75,7 +75,7 @@ Response: "data": [ { "type": "users", - "id": "abf488a9-9627-4c95-9170-dcfe2ce6b4bf", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person", "email": "Hello@authcompanion.com", @@ -92,7 +92,7 @@ Response: }, { "type": "users", - "id": "2ca7c207-f945-4aba-9a30-f106ff42cd22", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person 2", "email": "Hello2@authcompanion.com", @@ -154,7 +154,7 @@ Response: { "data": { "type": "users", - "id": "abf488a9-9627-4c95-9170-dcfe2ce6b4bf", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person", "email": "hello@authcompanion.com", @@ -210,7 +210,7 @@ Response: { "data": { "type": "users", - "id": "abf488a9-9627-4c95-9170-dcfe2ce6b4bf", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person", "email": "hello@authcompanion.com", @@ -243,3 +243,39 @@ Response: 204 No Content or 404 Not Found if no user is found. --- + +### admin/refresh + +Description: Your admin user's access token (JWTs) will expire according the exp date. When it does, you can refresh the access token without having to login your admin user again. + +If the request has a valid refresh token AuthCompanion will return a new access token and a new refresh token. + +Bearer Token Required: `Authorization: Bearer {admin access token}` +**POST** Request Body: + +```json +{ + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJ3ejVkY3RvMmJqNW9lb2Q2N2VoajN0dmwiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInVzZXJGaW5nZXJwcmludCI6bnVsbCwic2NvcGUiOiJhZG1pbiIsImlhdCI6MTY5Nzk5MTYyNiwiZXhwIjoxNjk4NTk2NDI2LCJqdGkiOiI0YjVjNmU1Ni1jMzA1LTQ3ZjQtYjEyNC01YzgwMGFjY2Y2YTgifQ.q4U0smkirdBKdsUBQMcszuqg4IXIpM49lHzdE6WEeLE" +} +``` + +Response: + +```json +{ + "data": { + "id": "wz5dcto2bj5oeod67ehj3tvl", + "type": "users", + "attributes": { + "name": "Admin", + "email": "admin@localhost", + "created": "2023-10-12T16:10:17.709Z", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJ3ejVkY3RvMmJqNW9lb2Q2N2VoajN0dmwiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInVzZXJGaW5nZXJwcmludCI6IiRhcmdvbjJpZCR2PTE5JG09NjQwMDAsdD0zLHA9MSQ2ZXV6U2xEbkQ1NmpJb3BkVVNHZ0RBJEZFZ1VRWm9WckJDLytpSmV6ZHlxR2FVelFDTXIyUEhmbmg0TkdIL3lOYTAiLCJzY29wZSI6ImFkbWluIiwiaWF0IjoxNjk3OTkxNjYxLCJleHAiOjE2OTc5OTUyNjF9.-EiyNfFFeHKpUCyEZZXvVcQ1vxqO-D34NWfS3HKF3jU", + "access_token_expiry": 1697995261, + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJ3ejVkY3RvMmJqNW9lb2Q2N2VoajN0dmwiLCJuYW1lIjoiQWRtaW4iLCJlbWFpbCI6ImFkbWluQGxvY2FsaG9zdCIsInVzZXJGaW5nZXJwcmludCI6bnVsbCwic2NvcGUiOiJhZG1pbiIsImlhdCI6MTY5Nzk5MTY2MSwiZXhwIjoxNjk4NTk2NDYxLCJqdGkiOiJiMTY3ODEzOS0yNGY0LTQ0YjctOTE0MC05NThlNDY0NjIyZWIifQ.v3QweztMbJLINqh9iWUahyUnOPC1EqefmEbc_IuK-Zk" + } + } +} +``` + +--- diff --git a/documentation/reference/authapi.md b/documentation/reference/authapi.md index 6bf0e6d..4d6bec9 100644 --- a/documentation/reference/authapi.md +++ b/documentation/reference/authapi.md @@ -43,7 +43,7 @@ Response: { "data": { "type": "users", - "id": "f1b84e9c-4e5d-4c4a-8571-e1577aefa968", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person", "email": "hello@authcompanion.com", @@ -82,7 +82,7 @@ Response: { "data": { "type": "users", - "id": "f1b84e9c-4e5d-4c4a-8571-e1577aefa968", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person", "email": "hello@authcompanion.com", @@ -131,7 +131,7 @@ Response: { "data": { "type": "users", - "id": "d6978388-07d4-4c79-ad7f-e26ee758eebc", + "id": "wz5dcto2bj5oeod67ehj3tvl", "attributes": { "name": "Authy Person_update", "email": "hello_update@authcompanion.com", diff --git a/documentation/reference/tokens.md b/documentation/reference/tokens.md new file mode 100644 index 0000000..ed4e124 --- /dev/null +++ b/documentation/reference/tokens.md @@ -0,0 +1,50 @@ +# Understanding Access Tokens and Refresh Tokens + +Access tokens and refresh tokens play a pivotal role in securing access to resources and ensuring a seamless user experience. In this article, we'll delve into these two tokens, their significance, and the claims they provide. + +## Access Tokens + +### What is an Access Token? + +Access tokens are short-lived, temporary credentials that grant access to specific resources or services within an application. These tokens are issued by AuthCompanion to ensure that users or applications have the required permissions to access protected resources/APIs. + +### Claims in an Access Token + +An access token includes several claims, which are pieces of information that provide essential details about the token and the user it represents. AuthC generates the following claims in the access token: + +- `userid`: A unique identifier for the user. +- `name`: The user's name. +- `email`: The user's email address. +- `scope`: The level of access or permissions granted by the token. A value of Admin means the access token is for the AuthC administrator. +- `metadata`: Public claims that can be set using the user's access token. These claims can provide additional information about the user or the application, and their content may vary based on the specific use case. +- `app`: A private claim that can be set in the user's JWT issued after login. This claim is changeable only using the admin access token and is used for specific application-related purposes. +- `iat` (issued at): The timestamp indicating when the token was issued. +- `exp` (expiration): The timestamp indicating when the token will expire. + +Access tokens default to a one-hour expiration time. + +Access tokens are short-lived, usually with a relatively short expiration time to enhance security. When a user or application presents an access token to access a protected resource, the system checks the claims to determine if the requested action is allowed. If the access token is valid and the claims permit the action, access is granted. The application will also verify the signature - JWTs are signed by AuthC to ensure their integrity and authenticity. The application uses the key generated by AuthC to verify the signature. + +## Refresh Tokens + +### What is a Refresh Token? + +While access tokens are short-lived, refresh tokens serve a different purpose. A refresh token is a long-lived credential that is used to obtain a new access token without the need for the user to re-enter their credentials. Refresh tokens are a crucial component of token-based authentication systems, ensuring a seamless user experience. + +### Claims in a Refresh Token + +Refresh tokens also include claims, and AuthC generates the following list fo claims: + +- `userid`: A unique identifier for the user. +- `name`: The user's name or identifier. +- `email`: The user's email address. +- `scope`: The level of access or permissions granted. +- `iat` (issued at): The timestamp indicating when the refresh token was issued. +- `exp` (expiration): The timestamp indicating when the refresh token will expire. Refresh tokens typically default to a seven-day expiration time. +- `jti` (JWT ID): A unique identifier for the refresh token. + +When a user's access token expires, applications can present the refresh token to AuthC and obtain a new access token without going through the full authentication process again. This mechanism ensures user convenience while maintaining security. + +## Token Expiration + +Token expiration is a critical aspect of access and refresh tokens. Access tokens typically default to a one-hour expiration time to limit their usability, while refresh tokens default to a seven-day expiration time. These default values strike a balance between security and user experience. However, in practice, you can configure token expiration times based on your application's specific security and usability requirements. diff --git a/package.json b/package.json index be3ee7d..f048273 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,16 @@ "license": "Big Time Public License", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", - "@simplewebauthn/server": "^8.1.1", + "@simplewebauthn/server": "^8.3.2", "argon2": "^0.31.1", - "better-sqlite3": "^8.6.0", + "better-sqlite3": "^8.7.0", "compadre": "^3.2.0", "cookie": "^0.5.0", "dotenv": "^16.3.1", "emailjs": "^4.0.3", - "fastify": "^4.23.2", + "fastify": "^4.24.3", "fastify-plugin": "^4.5.1", - "jose": "^4.14.6" + "jose": "^4.15.4" }, "devDependencies": { "@tailwindcss/forms": "^0.5.6", diff --git a/plugins/db/db.js b/plugins/db/db.js index d3d2eda..046c75b 100644 --- a/plugins/db/db.js +++ b/plugins/db/db.js @@ -9,7 +9,11 @@ const VERSION = 4; const migrate = (db, version) => { const allFiles = readdirSync("./plugins/db/schema/"); const sqlFiles = allFiles.filter((file) => extname(file) === ".sql"); - sqlFiles.sort(); + const collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: "base", + }); + sqlFiles.sort(collator.compare); if (version === null || version === undefined) { version = 1; } @@ -57,7 +61,9 @@ const dbPlugin = async function (fastify) { fastify.log.info(`Using Sqlite3 Database: ${config.DBPATH}`); } catch (error) { console.log(error); - throw new Error("There was an error setting and connecting up the Database, please try again!"); + throw new Error( + "There was an error setting and connecting up the Database, please try again!", + ); } //make available the database across the server by calling "db" fastify.decorate("db", db); diff --git a/plugins/key/admin.key.js b/plugins/key/admin.key.js index 0568125..8a513a6 100644 --- a/plugins/key/admin.key.js +++ b/plugins/key/admin.key.js @@ -19,7 +19,7 @@ const setupAdminKey = async function (fastify) { //Check if the admin user already exists on server startup const stmt = fastify.db.prepare( - "SELECT uuid, name, email, active, created_at, updated_at FROM admin LIMIT 1;" + "SELECT uuid, name, email, active, created_at, updated_at FROM admin LIMIT 1;", ); const adminUser = await stmt.get(); @@ -43,7 +43,7 @@ const setupAdminKey = async function (fastify) { }; const registerStmt = fastify.db.prepare( - "INSERT INTO admin (uuid, name, email, password, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, active, created_at, updated_at;" + "INSERT INTO admin (uuid, name, email, password, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, active, created_at, updated_at;", ); const user = registerStmt.get( uuid, @@ -51,13 +51,13 @@ const setupAdminKey = async function (fastify) { userObj.email, hashPwd, 1, - "" + "", ); //export admin password to a file. Admin password is only exported once on server startup and can be traded for an access token writeFileSync( config.ADMINKEYPATH, - `admin email: ${userObj.email} \nadmin password: ${adminPwd}` + `admin email: ${userObj.email} \nadmin password: ${adminPwd}`, ); fastify.log.info(`Generating Admin API Key: ${config.ADMINKEYPATH}...`); fastify.log.info(`Admin Email: ${userObj.email} & Password: ${adminPwd}`); diff --git a/plugins/key/server.key.js b/plugins/key/server.key.js index d08febd..6e75585 100644 --- a/plugins/key/server.key.js +++ b/plugins/key/server.key.js @@ -20,7 +20,7 @@ export async function importKey() { hash: "SHA-256", }, true, - ["sign", "verify"] + ["sign", "verify"], ); return key; } catch (error) { @@ -37,7 +37,7 @@ async function generateAndExportKey() { length: 512, }, true, - ["sign", "verify"] + ["sign", "verify"], ); const rawKey = await subtle.exportKey("jwk", key); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3569d7..959a8c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,14 +9,14 @@ dependencies: specifier: ^2.2.2 version: 2.2.2 '@simplewebauthn/server': - specifier: ^8.1.1 - version: 8.1.1 + specifier: ^8.3.2 + version: 8.3.2 argon2: specifier: ^0.31.1 version: 0.31.1 better-sqlite3: - specifier: ^8.6.0 - version: 8.6.0 + specifier: ^8.7.0 + version: 8.7.0 compadre: specifier: ^3.2.0 version: 3.2.0 @@ -30,14 +30,14 @@ dependencies: specifier: ^4.0.3 version: 4.0.3 fastify: - specifier: ^4.23.2 - version: 4.23.2 + specifier: ^4.24.3 + version: 4.24.3 fastify-plugin: specifier: ^4.5.1 version: 4.5.1 jose: - specifier: ^4.14.6 - version: 4.14.6 + specifier: ^4.15.4 + version: 4.15.4 devDependencies: '@tailwindcss/forms': @@ -145,8 +145,8 @@ packages: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: false - /@fastify/error@3.3.0: - resolution: {integrity: sha512-dj7vjIn1Ar8sVXj2yAXiMNCJDmS9MQ9XMlIecX2dIzzhjSHCyKo4DdXjXMs7wKW2kj6yvVRSpuQjOZ3YLrh56w==} + /@fastify/error@3.4.0: + resolution: {integrity: sha512-e/mafFwbK3MNqxUcFBLgHhgxsF8UT1m8aj0dAlqEa2nJEgPsRtpHTZ3ObgrgkZ2M1eJHPTwgyUl/tXkvabsZdQ==} dev: false /@fastify/fast-json-stringify-compiler@4.3.0: @@ -170,7 +170,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.20 dev: true /@jridgewell/resolve-uri@3.1.1: @@ -187,8 +187,8 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@jridgewell/trace-mapping@0.3.19: - resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 @@ -244,44 +244,44 @@ packages: '@noble/hashes': 1.3.2 dev: false - /@peculiar/asn1-android@2.3.6: - resolution: {integrity: sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g==} + /@peculiar/asn1-android@2.3.10: + resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==} dependencies: - '@peculiar/asn1-schema': 2.3.6 + '@peculiar/asn1-schema': 2.3.8 asn1js: 3.0.5 tslib: 2.6.2 dev: false - /@peculiar/asn1-ecc@2.3.6: - resolution: {integrity: sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==} + /@peculiar/asn1-ecc@2.3.8: + resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==} dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/asn1-x509': 2.3.6 + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 asn1js: 3.0.5 tslib: 2.6.2 dev: false - /@peculiar/asn1-rsa@2.3.6: - resolution: {integrity: sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==} + /@peculiar/asn1-rsa@2.3.8: + resolution: {integrity: sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==} dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/asn1-x509': 2.3.6 + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 asn1js: 3.0.5 tslib: 2.6.2 dev: false - /@peculiar/asn1-schema@2.3.6: - resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} + /@peculiar/asn1-schema@2.3.8: + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} dependencies: asn1js: 3.0.5 pvtsutils: 1.3.5 tslib: 2.6.2 dev: false - /@peculiar/asn1-x509@2.3.6: - resolution: {integrity: sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==} + /@peculiar/asn1-x509@2.3.8: + resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==} dependencies: - '@peculiar/asn1-schema': 2.3.6 + '@peculiar/asn1-schema': 2.3.8 asn1js: 3.0.5 ipaddr.js: 2.1.0 pvtsutils: 1.3.5 @@ -293,16 +293,16 @@ packages: engines: {node: '>=10'} dev: false - /@simplewebauthn/server@8.1.1: - resolution: {integrity: sha512-fJ0Ux9eV5oLa6gowHiUXx+oDqh6DhDK/w1oenn8p9+MhZDCXtLOIWl3Crgq5FLnwOuX9NpJzHgmgaOk2b8Tojg==} + /@simplewebauthn/server@8.3.2: + resolution: {integrity: sha512-ceo8t5gdO5W/JOePQWPDH+rAd8tO6QNalLU56rc9ItdzaTjk+qcYwQg/BKXDDg6117P3HKrRBkZwBrMJl4dOdA==} engines: {node: '>=16.0.0'} dependencies: '@hexagon/base64': 1.1.28 - '@peculiar/asn1-android': 2.3.6 - '@peculiar/asn1-ecc': 2.3.6 - '@peculiar/asn1-rsa': 2.3.6 - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/asn1-x509': 2.3.6 + '@peculiar/asn1-android': 2.3.10 + '@peculiar/asn1-ecc': 2.3.8 + '@peculiar/asn1-rsa': 2.3.8 + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 '@simplewebauthn/typescript-types': 8.0.0 cbor-x: 1.5.4 cross-fetch: 4.0.0 @@ -323,12 +323,12 @@ packages: tailwindcss: 3.3.3 dev: true - /@types/minimist@1.2.2: - resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + /@types/minimist@1.2.4: + resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} dev: true - /@types/normalize-package-data@2.4.1: - resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + /@types/normalize-package-data@2.4.3: + resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==} dev: true /JSONStream@1.3.5: @@ -543,7 +543,7 @@ packages: chalk: 5.3.0 chokidar: 3.5.3 chunkd: 2.0.1 - ci-info: 3.8.0 + ci-info: 3.9.0 ci-parallel-vars: 1.0.1 clean-yaml-object: 0.1.0 cli-truncate: 3.1.0 @@ -597,8 +597,8 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false - /better-sqlite3@8.6.0: - resolution: {integrity: sha512-jwAudeiTMTSyby+/SfbHDebShbmC2MCH8mU2+DXi0WJfv13ypEJm47cd3kljmy/H130CazEvkf2Li//ewcMJ1g==} + /better-sqlite3@8.7.0: + resolution: {integrity: sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==} requiresBuild: true dependencies: bindings: 1.5.0 @@ -750,8 +750,8 @@ packages: resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} dev: true - /ci-info@3.8.0: - resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} dev: true @@ -1268,25 +1268,25 @@ packages: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false - /fastify@4.23.2: - resolution: {integrity: sha512-WFSxsHES115svC7NrerNqZwwM0UOxbC/P6toT9LRHgAAFvG7o2AN5W+H4ihCtOGuYXjZf4z+2jXC89rVEoPWOA==} + /fastify@4.24.3: + resolution: {integrity: sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==} dependencies: '@fastify/ajv-compiler': 3.5.0 - '@fastify/error': 3.3.0 + '@fastify/error': 3.4.0 '@fastify/fast-json-stringify-compiler': 4.3.0 abstract-logging: 2.0.1 avvio: 8.2.1 fast-content-type-parse: 1.1.0 fast-json-stringify: 5.8.0 - find-my-way: 7.6.2 + find-my-way: 7.7.0 light-my-request: 5.11.0 - pino: 8.15.1 + pino: 8.16.1 process-warning: 2.2.0 proxy-addr: 2.0.7 rfdc: 1.3.0 secure-json-parse: 2.7.0 semver: 7.5.4 - toad-cache: 3.2.0 + toad-cache: 3.3.0 transitivePeerDependencies: - supports-color dev: false @@ -1315,8 +1315,8 @@ packages: to-regex-range: 5.0.1 dev: true - /find-my-way@7.6.2: - resolution: {integrity: sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==} + /find-my-way@7.7.0: + resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} engines: {node: '>=14'} dependencies: fast-deep-equal: 3.1.3 @@ -1374,8 +1374,8 @@ packages: dev: true optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true /gauge@3.0.2: @@ -1524,11 +1524,11 @@ packages: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 dev: true /hosted-git-info@2.8.9: @@ -1619,10 +1619,10 @@ packages: binary-extensions: 2.2.0 dev: true - /is-core-module@2.13.0: - resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - has: 1.0.3 + hasown: 2.0.0 dev: true /is-error@2.2.2: @@ -1695,8 +1695,8 @@ packages: hasBin: true dev: true - /jose@4.14.6: - resolution: {integrity: sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ==} + /jose@4.15.4: + resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} dev: false /js-string-escape@1.0.1: @@ -1860,7 +1860,7 @@ packages: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} dependencies: - '@types/minimist': 1.2.2 + '@types/minimist': 1.2.4 camelcase-keys: 6.2.2 decamelize-keys: 1.1.1 hard-rejection: 2.1.0 @@ -1987,8 +1987,8 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /node-abi@3.47.0: - resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} + /node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} engines: {node: '>=10'} dependencies: semver: 7.5.4 @@ -2033,7 +2033,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.6 + resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -2043,7 +2043,7 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.13.0 + is-core-module: 2.13.1 semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true @@ -2071,8 +2071,9 @@ packages: engines: {node: '>= 6'} dev: true - /on-exit-leak-free@2.1.0: - resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} dev: false /once@1.4.0: @@ -2244,21 +2245,21 @@ packages: resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} dev: false - /pino@8.15.1: - resolution: {integrity: sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==} + /pino@8.16.1: + resolution: {integrity: sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==} hasBin: true dependencies: atomic-sleep: 1.0.0 fast-redact: 3.3.0 - on-exit-leak-free: 2.1.0 + on-exit-leak-free: 2.1.2 pino-abstract-transport: 1.1.0 pino-std-serializers: 6.2.2 process-warning: 2.2.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.4.3 - sonic-boom: 3.3.0 - thread-stream: 2.4.0 + sonic-boom: 3.7.0 + thread-stream: 2.4.1 dev: false /pirates@4.0.6: @@ -2281,29 +2282,29 @@ packages: irregular-plurals: 3.5.0 dev: true - /postcss-import@15.1.0(postcss@8.4.30): + /postcss-import@15.1.0(postcss@8.4.31): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.30 + postcss: 8.4.31 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.6 + resolve: 1.22.8 dev: true - /postcss-js@4.0.1(postcss@8.4.30): + /postcss-js@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.30 + postcss: 8.4.31 dev: true - /postcss-load-config@4.0.1(postcss@8.4.30): + /postcss-load-config@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} peerDependencies: @@ -2316,17 +2317,17 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - postcss: 8.4.30 - yaml: 2.3.2 + postcss: 8.4.31 + yaml: 2.3.3 dev: true - /postcss-nested@6.0.1(postcss@8.4.30): + /postcss-nested@6.0.1(postcss@8.4.31): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.30 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 dev: true @@ -2342,8 +2343,8 @@ packages: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} dev: true - /postcss@8.4.30: - resolution: {integrity: sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==} + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 @@ -2362,7 +2363,7 @@ packages: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 1.0.2 - node-abi: 3.47.0 + node-abi: 3.51.0 pump: 3.0.0 rc: 1.2.8 simple-get: 4.0.1 @@ -2486,7 +2487,7 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} dependencies: - '@types/normalize-package-data': 2.4.1 + '@types/normalize-package-data': 2.4.3 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 @@ -2565,11 +2566,11 @@ packages: engines: {node: '>=8'} dev: true - /resolve@1.22.6: - resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.13.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -2688,8 +2689,8 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true - /sonic-boom@3.3.0: - resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} + /sonic-boom@3.7.0: + resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==} dependencies: atomic-sleep: 1.0.0 dev: false @@ -2708,7 +2709,7 @@ packages: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.15 + spdx-license-ids: 3.0.16 dev: true /spdx-exceptions@2.3.0: @@ -2719,11 +2720,11 @@ packages: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.15 + spdx-license-ids: 3.0.16 dev: true - /spdx-license-ids@3.0.15: - resolution: {integrity: sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==} + /spdx-license-ids@3.0.16: + resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} dev: true /split2@3.2.2: @@ -2867,13 +2868,13 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.30 - postcss-import: 15.1.0(postcss@8.4.30) - postcss-js: 4.0.1(postcss@8.4.30) - postcss-load-config: 4.0.1(postcss@8.4.30) - postcss-nested: 6.0.1(postcss@8.4.30) + postcss: 8.4.31 + postcss-import: 15.1.0(postcss@8.4.31) + postcss-js: 4.0.1(postcss@8.4.31) + postcss-load-config: 4.0.1(postcss@8.4.31) + postcss-nested: 6.0.1(postcss@8.4.31) postcss-selector-parser: 6.0.13 - resolve: 1.22.6 + resolve: 1.22.8 sucrase: 3.34.0 transitivePeerDependencies: - ts-node @@ -2947,8 +2948,8 @@ packages: any-promise: 1.3.0 dev: true - /thread-stream@2.4.0: - resolution: {integrity: sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==} + /thread-stream@2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} dependencies: real-require: 0.2.0 dev: false @@ -2976,8 +2977,8 @@ packages: is-number: 7.0.0 dev: true - /toad-cache@3.2.0: - resolution: {integrity: sha512-Hj5zSqBS6OHbZoQk9IU8VqIr+0JUpwzunnwSlFJhG8aJSInYUMEuzItl3kJsGteTPd1qtflafdRHlRtUazYeqg==} + /toad-cache@3.3.0: + resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} engines: {node: '>=12'} dev: false @@ -3113,8 +3114,8 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yaml@2.3.2: - resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==} + /yaml@2.3.3: + resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==} engines: {node: '>= 14'} dev: true diff --git a/routes/admin.routes.js b/routes/admin.routes.js index 3142e48..69da1a6 100644 --- a/routes/admin.routes.js +++ b/routes/admin.routes.js @@ -16,6 +16,9 @@ import { loginSchema } from "../services/admin/users/schema/loginSchema.js"; import { logoutHandler } from "../services/admin/users/logout.js"; +import { tokenRefreshHandler } from "../services/admin/users/refresh.js"; +import { refreshSchema } from "../services/admin/users/schema/refreshSchema.js"; + import { authenticateAdminRequest, authenticateWebAdminRequest, @@ -26,29 +29,30 @@ const adminRoutes = async function (fastify, options) { fastify.post( "/users", { onRequest: [authenticateAdminRequest], ...createSchema }, - createUserHandler + createUserHandler, ); fastify.get( "/users", { onRequest: [authenticateAdminRequest], ...listUsersSchema }, - listUsersHandler + listUsersHandler, ); fastify.delete( "/users/:uuid", { onRequest: [authenticateAdminRequest] }, - deleteUserHandler + deleteUserHandler, ); fastify.patch( "/users/:uuid", { onRequest: [authenticateAdminRequest], ...updateSchema }, - updateUserHandler + updateUserHandler, ); fastify.post("/login", loginSchema, loginHandler); fastify.get( "/logout", { onRequest: [authenticateAdminRequest] }, - logoutHandler + logoutHandler, ); + fastify.post("/refresh", refreshSchema, tokenRefreshHandler); //admin web user interface routes fastify.get( @@ -60,7 +64,7 @@ const adminRoutes = async function (fastify, options) { "Content-Type": `text/html`, }); return adminPage; - } + }, ); //login page for the admin web user interface diff --git a/routes/auth.routes.js b/routes/auth.routes.js index 757873c..0574514 100644 --- a/routes/auth.routes.js +++ b/routes/auth.routes.js @@ -30,7 +30,7 @@ const authRoutes = async function (fastify, options) { fastify.post( "/users/me", { onRequest: [authenticateAuthRequest], ...userProfileSchema }, - userProfileHandler + userProfileHandler, ); fastify.post("/recovery", profileRecoverySchema, profileRecoveryHandler); fastify.post("/refresh", refreshSchema, tokenRefreshHandler); @@ -40,13 +40,13 @@ const authRoutes = async function (fastify, options) { fastify.post( "/registration-verification", registerVerificationSchema, - registrationVerificationHandler + registrationVerificationHandler, ); fastify.get("/login-options", loginOptionsHandler); fastify.post( "/login-verification", loginVerificationSchema, - loginVerificationHandler + loginVerificationHandler, ); }; diff --git a/routes/ui.routes.js b/routes/ui.routes.js index 673ba64..49ec861 100644 --- a/routes/ui.routes.js +++ b/routes/ui.routes.js @@ -15,7 +15,7 @@ const webRoutes = async function (fastify, options) { fastify.get("/register", (request, reply) => { //create session id for tracking webauthn challenges used for verification. Send session id as cookie const registrationPage = readFileSync( - "./client/auth/registrationPage.html" + "./client/auth/registrationPage.html", ); reply.headers({ "Content-Type": `text/html`, diff --git a/services/admin/users/create.js b/services/admin/users/create.js index bd9b37c..2ac4988 100644 --- a/services/admin/users/create.js +++ b/services/admin/users/create.js @@ -6,7 +6,9 @@ export const createUserHandler = async function (request, reply) { try { //Check the request's type attibute is set to users if (request.body.data.type !== "users") { - request.log.info("Admin API: The request's type is not set to Users, creation failed"); + request.log.info( + "Admin API: The request's type is not set to Users, creation failed", + ); throw { statusCode: 400, message: "Invalid Type Attribute" }; } @@ -15,24 +17,34 @@ export const createUserHandler = async function (request, reply) { const duplicateAccount = await stmt.get(request.body.data.attributes.email); if (duplicateAccount) { - request.log.info("Admin API: User's email already exists in database, creation failed"); + request.log.info( + "Admin API: User's email already exists in database, creation failed", + ); throw { statusCode: 400, message: "Duplicate Email Address Exists" }; } //If the user's active status is a string, convert it to a number if (request.body.data.attributes.active) { if (typeof request.body.data.attributes.active === "string") { - request.body.data.attributes.active = Number(request.body.data.attributes.active); + request.body.data.attributes.active = Number( + request.body.data.attributes.active, + ); } } //Check if the user's active status is being updated and if it is, check if the new status is a valid 1 or 0 if (request.body.data.attributes.active) { - if (request.body.data.attributes.active !== 0 && request.body.data.attributes.active !== 1) { - request.log.info("Admin API: User's active status is not valid, update failed"); + if ( + request.body.data.attributes.active !== 0 && + request.body.data.attributes.active !== 1 + ) { + request.log.info( + "Admin API: User's active status is not valid, update failed", + ); throw { statusCode: 400, - message: "Invalid Active Status, Please use 1 for true and 0 for false", + message: + "Invalid Active Status, Please use 1 for true and 0 for false", }; } } @@ -43,7 +55,7 @@ export const createUserHandler = async function (request, reply) { const jwtid = randomUUID(); const registerStmt = this.db.prepare( - "INSERT INTO users (uuid, name, email, password, metadata, appdata, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, metadata, appdata, active, created_at, updated_at;" + "INSERT INTO users (uuid, name, email, password, metadata, appdata, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, metadata, appdata, active, created_at, updated_at;", ); const user = registerStmt.get( uuid, @@ -53,7 +65,7 @@ export const createUserHandler = async function (request, reply) { JSON.stringify(request.body.data.attributes.metadata), JSON.stringify(request.body.data.attributes.app), request.body.data.attributes.active, - jwtid + jwtid, ); //Prepare the server response const userAttributes = { diff --git a/services/admin/users/delete.js b/services/admin/users/delete.js index 2615b73..fd2dcbc 100644 --- a/services/admin/users/delete.js +++ b/services/admin/users/delete.js @@ -6,7 +6,7 @@ export const deleteUserHandler = async function (request, reply) { if (!user) { request.log.info( - "Admin API: User does not exist in database, delete failed" + "Admin API: User does not exist in database, delete failed", ); throw { statusCode: 404, message: "User Not Found" }; } @@ -14,7 +14,7 @@ export const deleteUserHandler = async function (request, reply) { //check if the user's uuid is equal to the admin this.registeredAdminUser.uuid (the admin user cannot be deleted) if (user.uuid === this.registeredAdminUser.uuid) { request.log.info( - "Admin API: Admin user cannot be deleted, delete failed" + "Admin API: Admin user cannot be deleted, delete failed", ); throw { statusCode: 400, message: "Admin User Cannot Be Deleted" }; } diff --git a/services/admin/users/list.js b/services/admin/users/list.js index 41902da..c9962eb 100644 --- a/services/admin/users/list.js +++ b/services/admin/users/list.js @@ -1,6 +1,10 @@ export const listUsersHandler = async function (request, reply) { try { - const { "page[number]": pageNumber = 1, "page[size]": pageSize = 10, "search[email]": searchEmail } = request.query; + const { + "page[number]": pageNumber = 1, + "page[size]": pageSize = 10, + "search[email]": searchEmail, + } = request.query; // Convert the page number and size to integers const page = parseInt(pageNumber); @@ -10,7 +14,8 @@ export const listUsersHandler = async function (request, reply) { const offset = (page - 1) * size; // Prepare the SQL query and parameters - let query = "SELECT uuid, name, email, metadata, appdata, active, created_at, updated_at FROM users"; + let query = + "SELECT uuid, name, email, metadata, appdata, active, created_at, updated_at FROM users"; let params = []; if (searchEmail) { @@ -50,7 +55,9 @@ export const listUsersHandler = async function (request, reply) { } const countStmt = this.db.prepare(countQuery); - const totalCount = await countStmt.get(...(searchEmail ? [`%${searchEmail}%`] : [])); // Spread the searchEmail param if provided + const totalCount = await countStmt.get( + ...(searchEmail ? [`%${searchEmail}%`] : []), + ); // Spread the searchEmail param if provided // Calculate pagination metadata const totalPages = Math.ceil(totalCount.count / size); @@ -61,16 +68,24 @@ export const listUsersHandler = async function (request, reply) { const paginationLinks = {}; if (hasNextPage) { - paginationLinks.next = `${request.raw.url.split("?")[0]}?page[number]=${page + 1}&page[size]=${size}`; + paginationLinks.next = `${request.raw.url.split("?")[0]}?page[number]=${ + page + 1 + }&page[size]=${size}`; } if (hasPreviousPage) { - paginationLinks.prev = `${request.raw.url.split("?")[0]}?page[number]=${page - 1}&page[size]=${size}`; + paginationLinks.prev = `${request.raw.url.split("?")[0]}?page[number]=${ + page - 1 + }&page[size]=${size}`; } if (totalPages > 0) { - paginationLinks.first = `${request.raw.url.split("?")[0]}?page[number]=1&page[size]=${size}`; - paginationLinks.last = `${request.raw.url.split("?")[0]}?page[number]=${totalPages}&page[size]=${size}`; + paginationLinks.first = `${ + request.raw.url.split("?")[0] + }?page[number]=1&page[size]=${size}`; + paginationLinks.last = `${ + request.raw.url.split("?")[0] + }?page[number]=${totalPages}&page[size]=${size}`; } // Send the server reply diff --git a/services/admin/users/login.js b/services/admin/users/login.js index 3cbc3e4..97ddaf4 100644 --- a/services/admin/users/login.js +++ b/services/admin/users/login.js @@ -1,43 +1,51 @@ -import { verifyValueWithHash } from '../../../utils/credential.js'; -import { makeAdminToken } from '../../../utils/jwt.js'; -import config from '../../../config.js'; +import { verifyValueWithHash } from "../../../utils/credential.js"; +import { makeAdminToken, makeAdminRefreshtoken } from "../../../utils/jwt.js"; +import config from "../../../config.js"; export const loginHandler = async function (request, reply) { try { // Check the request's type attibute is set to users - if (request.body.data.type !== 'users') { - request.log.info("Auth API: The request's type is not set to Users, creation failed"); - throw { statusCode: 400, message: 'Invalid Type Attribute' }; + if (request.body.data.type !== "users") { + request.log.info( + "Auth API: The request's type is not set to Users, creation failed", + ); + throw { statusCode: 400, message: "Invalid Type Attribute" }; } - // Fetch the registered admin user from the database with + // Fetch the registered admin user from the database const stmt = this.db.prepare( - 'SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM admin WHERE email = ?;' + "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM admin WHERE email = ?;", ); const userObj = await stmt.get(request.body.data.attributes.email); // Check if admin user does not exist in the database if (!userObj) { - request.log.info('Auth API: User does not exist in database, login failed'); - throw { statusCode: 400, message: 'Login Failed' }; + request.log.info( + "Auth API: User does not exist in database, login failed", + ); + throw { statusCode: 400, message: "Login Failed" }; } // Check if user has an 'active' account if (!userObj.active) { - request.log.info('Auth API: User account is not active, login failed'); - throw { statusCode: 400, message: 'Login Failed' }; + request.log.info("Auth API: User account is not active, login failed"); + throw { statusCode: 400, message: "Login Failed" }; } - const passwordCheckResult = await verifyValueWithHash(request.body.data.attributes.password, userObj.password); + const passwordCheckResult = await verifyValueWithHash( + request.body.data.attributes.password, + userObj.password, + ); // Check if user has the correct password if (!passwordCheckResult) { - request.log.info('Auth API: User password is incorrect, login failed'); - throw { statusCode: 400, message: 'Login Failed' }; + request.log.info("Auth API: User password is incorrect, login failed"); + throw { statusCode: 400, message: "Login Failed" }; } // Looks good! Let's prepare the reply const adminAccessToken = await makeAdminToken(userObj, this.key); + const adminRefreshToken = await makeAdminRefreshtoken(userObj, this.key); const userAttributes = { name: userObj.name, @@ -45,21 +53,18 @@ export const loginHandler = async function (request, reply) { created: userObj.created_at, access_token: adminAccessToken.token, access_token_expiry: adminAccessToken.expiration, + refresh_token: adminRefreshToken.token, }; const expireDate = new Date(); expireDate.setTime(expireDate.getTime() + 7 * 24 * 60 * 60 * 1000); // TODO: Make configurable now, set to 7 days reply.headers({ - 'set-cookie': [ - `adminAccessToken=${adminAccessToken.token}; Path=/; Expires=${expireDate}; SameSite=None; Secure; HttpOnly`, - `Fgp=${adminAccessToken.userFingerprint}; Path=/; Max-Age=7200; SameSite=None; Secure; HttpOnly`, - ], - 'x-authc-app-origin': config.ADMINORIGIN, + "x-authc-app-origin": config.ADMINORIGIN, }); return { data: { - type: 'users', + type: "users", id: userObj.uuid, attributes: userAttributes, }, diff --git a/services/admin/users/logout.js b/services/admin/users/logout.js index d0e01a2..d3c44d2 100644 --- a/services/admin/users/logout.js +++ b/services/admin/users/logout.js @@ -2,20 +2,21 @@ import config from "../../../config.js"; export const logoutHandler = async function (request, reply) { try { - const expireDate = new Date(); - expireDate.setTime(expireDate.getTime()); + const jwtPayload = request.jwtRequestPayload; + const userId = jwtPayload.userid; + + //Check if the user exists in the database + const stmt = this.db.prepare("UPDATE admin SET jwt_id = 0 WHERE uuid = ?;"); + const result = await stmt.run(userId); reply.headers({ - "set-cookie": `adminAccessToken=; Path=/; Expires=; SameSite=None; Secure; HttpOnly`, "x-authc-app-origin": config.ADMINORIGIN, }); return { - data: { - logout: true, - }, + logout: true, }; } catch (err) { - throw { statusCode: err.statusCode, message: err.message }; + throw { statusCode: err.statusCode, message: "Server Error" }; } }; diff --git a/services/admin/users/refresh.js b/services/admin/users/refresh.js new file mode 100644 index 0000000..531bc95 --- /dev/null +++ b/services/admin/users/refresh.js @@ -0,0 +1,74 @@ +import { + makeAdminToken, + makeAdminRefreshtoken, + validateJWT, +} from "../../../utils/jwt.js"; +import config from "../../../config.js"; + +export const tokenRefreshHandler = async function (request, reply) { + try { + let refreshToken = ""; + // Check if the request body contains a refresh token + if (request.body && request.body.refreshToken) { + refreshToken = request.body.refreshToken; + } else { + request.log.info( + "Admin API: The request does not include a refresh token in the request body, refresh token failed", + ); + throw { + statusCode: 400, + message: + "The request does not include a refresh token in the request body.", + }; + } + + // Validate the refresh token + const jwtClaims = await validateJWT(refreshToken, this.key); + + // Fetch the registered admin user from the database + const stmt = this.db.prepare( + "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM admin WHERE uuid = ?;", + ); + const userObj = await stmt.get(jwtClaims.userid); + + // Check if the "jiti" value in the JWT payload matches the admin's "jwt_id" + if (jwtClaims.jti !== userObj.jwt_id) { + request.log.info( + "Admin API: JWT jiti value does not match the admin's jwt_id", + ); + throw { + statusCode: 401, + message: "Server Error", + }; + } + + // Looks good! Let's prepare the reply + const adminAccessToken = await makeAdminToken(userObj, this.key); + const adminRefreshToken = await makeAdminRefreshtoken(userObj, this.key); + + const userAttributes = { + name: userObj.name, + email: userObj.email, + created: userObj.created_at, + access_token: adminAccessToken.token, + access_token_expiry: adminAccessToken.expiration, + refresh_token: adminRefreshToken.token, + }; + const expireDate = new Date(); + expireDate.setTime(expireDate.getTime() + 7 * 24 * 60 * 60 * 1000); // TODO: Make configurable now, set to 7 days + + reply.headers({ + "x-authc-app-origin": config.APPLICATIONORIGIN, + }); + + return { + data: { + id: userObj.uuid, + type: "users", + attributes: userAttributes, + }, + }; + } catch (err) { + throw { statusCode: err.statusCode, message: err.message }; + } +}; diff --git a/services/admin/users/schema/refreshSchema.js b/services/admin/users/schema/refreshSchema.js new file mode 100644 index 0000000..75dd06c --- /dev/null +++ b/services/admin/users/schema/refreshSchema.js @@ -0,0 +1,11 @@ +export const refreshSchema = { + schema: { + body: { + type: "object", + properties: { + refreshToken: { type: "string" }, // Add refreshToken property validation + }, + required: ["refreshToken"], // Mark refreshToken as required + }, + }, +}; diff --git a/services/admin/users/update.js b/services/admin/users/update.js index 64eb56e..3056506 100644 --- a/services/admin/users/update.js +++ b/services/admin/users/update.js @@ -4,7 +4,9 @@ export const updateUserHandler = async function (request, reply) { try { //Check the request's type attibute is set to users if (request.body.data.type !== "users") { - request.log.info("Admin API: The request's type is not set to Users, update failed"); + request.log.info( + "Admin API: The request's type is not set to Users, update failed", + ); throw { statusCode: 400, message: "Invalid Type Attribute" }; } @@ -13,17 +15,24 @@ export const updateUserHandler = async function (request, reply) { const user = await userStmt.get(request.params.uuid); if (!user) { - request.log.info("Admin API: User does not exist in database, update failed"); + request.log.info( + "Admin API: User does not exist in database, update failed", + ); throw { statusCode: 404, message: "User Not Found" }; } //Check if the user's email is being updated and if its not the same email as the user's current email, check if the new email is already in use - if (request.body.data.attributes.email && request.body.data.attributes.email !== user.email) { + if ( + request.body.data.attributes.email && + request.body.data.attributes.email !== user.email + ) { const emailStmt = this.db.prepare("SELECT * FROM users WHERE email = ?;"); const email = await emailStmt.get(request.body.data.attributes.email); if (email) { - request.log.info("Admin API: User's email is already in use, update failed"); + request.log.info( + "Admin API: User's email is already in use, update failed", + ); throw { statusCode: 400, message: "Email Already In Use" }; } } @@ -37,24 +46,32 @@ export const updateUserHandler = async function (request, reply) { //If the user's active status is a string, convert it to a number if (request.body.data.attributes.active) { if (typeof request.body.data.attributes.active === "string") { - request.body.data.attributes.active = Number(request.body.data.attributes.active); + request.body.data.attributes.active = Number( + request.body.data.attributes.active, + ); } } //Check if the user's active status is being updated and if it is, check if the new status is a valid 1 or 0 if (request.body.data.attributes.active) { - if (request.body.data.attributes.active !== 0 && request.body.data.attributes.active !== 1) { - request.log.info("Admin API: User's active status is not valid, update failed"); + if ( + request.body.data.attributes.active !== 0 && + request.body.data.attributes.active !== 1 + ) { + request.log.info( + "Admin API: User's active status is not valid, update failed", + ); throw { statusCode: 400, - message: "Invalid Active Status, Please use 1 for true and 0 for false", + message: + "Invalid Active Status, Please use 1 for true and 0 for false", }; } } //Per json-api spec: If a request does not include all of the attributes for a resource, the server MUST interpret the missing attributes as if they were included with their current values. The server MUST NOT interpret missing attributes as null values. const updateStmt = this.db.prepare( - "UPDATE users SET name = coalesce(?, name), email = coalesce(?, email), password = coalesce(?, password), metadata = coalesce(?, metadata), appdata = coalesce(?, appdata), active = coalesce(?, active), updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ? RETURNING uuid, name, email, metadata, appdata, active, created_at, updated_at;" + "UPDATE users SET name = coalesce(?, name), email = coalesce(?, email), password = coalesce(?, password), metadata = coalesce(?, metadata), appdata = coalesce(?, appdata), active = coalesce(?, active), updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ? RETURNING uuid, name, email, metadata, appdata, active, created_at, updated_at;", ); const updatedUser = updateStmt.get( request.body.data.attributes.name, @@ -63,7 +80,7 @@ export const updateUserHandler = async function (request, reply) { JSON.stringify(request.body.data.attributes.metadata), JSON.stringify(request.body.data.attributes.app), request.body.data.attributes.active, - request.params.uuid + request.params.uuid, ); //Prepare the server response diff --git a/services/auth/login.js b/services/auth/login.js index 0d7995b..b7c82df 100644 --- a/services/auth/login.js +++ b/services/auth/login.js @@ -6,19 +6,23 @@ export const loginHandler = async function (request, reply) { try { // Check the request's type attibute is set to users if (request.body.data.type !== "users") { - request.log.info("Auth API: The request's type is not set to Users, creation failed"); + request.log.info( + "Auth API: The request's type is not set to Users, creation failed", + ); throw { statusCode: 400, message: "Invalid Type Attribute" }; } // Fetch user from database const stmt = this.db.prepare( - "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at, metadata, appdata FROM users WHERE email = ?;" + "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at, metadata, appdata FROM users WHERE email = ?;", ); const userObj = await stmt.get(request.body.data.attributes.email); // Check if user does not exist in the database if (!userObj) { - request.log.info("Auth API: User does not exist in database, login failed"); + request.log.info( + "Auth API: User does not exist in database, login failed", + ); throw { statusCode: 400, message: "Login Failed" }; } @@ -28,7 +32,10 @@ export const loginHandler = async function (request, reply) { throw { statusCode: 400, message: "Login Failed" }; } - const passwordCheckResult = await verifyValueWithHash(request.body.data.attributes.password, userObj.password); + const passwordCheckResult = await verifyValueWithHash( + request.body.data.attributes.password, + userObj.password, + ); // Check if user has the correct password if (!passwordCheckResult) { diff --git a/services/auth/profile.js b/services/auth/profile.js index 549e2e5..74aec84 100644 --- a/services/auth/profile.js +++ b/services/auth/profile.js @@ -6,7 +6,9 @@ export const userProfileHandler = async function (request, reply) { try { //Check the request's type attibute is set to users if (request.body.data.type !== "users") { - request.log.info("Auth API: The request's type is not set to Users, update failed"); + request.log.info( + "Auth API: The request's type is not set to Users, update failed", + ); throw { statusCode: 400, message: "Invalid Type Attribute" }; } @@ -16,7 +18,9 @@ export const userProfileHandler = async function (request, reply) { //Check if the user exists already if (!user) { - request.log.info("Auth API: User does not exist in database, update failed"); + request.log.info( + "Auth API: User does not exist in database, update failed", + ); throw { statusCode: 400, message: "Profile Update Failed" }; } @@ -28,14 +32,14 @@ export const userProfileHandler = async function (request, reply) { //Per json-api spec: If a request does not include all of the attributes for a resource, the server MUST interpret the missing attributes as if they were included with their current values. The server MUST NOT interpret missing attributes as null values. const updateStmt = this.db.prepare( - "UPDATE users SET name = coalesce(?, name), email = coalesce(?, email), password = coalesce(?, password), metadata = coalesce(?, metadata), updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ? RETURNING uuid, name, email, metadata, appdata, jwt_id, active, created_at, updated_at;" + "UPDATE users SET name = coalesce(?, name), email = coalesce(?, email), password = coalesce(?, password), metadata = coalesce(?, metadata), updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ? RETURNING uuid, name, email, metadata, appdata, jwt_id, active, created_at, updated_at;", ); const userObj = updateStmt.get( request.body.data.attributes.name, request.body.data.attributes.email, request.body.data.attributes.password, JSON.stringify(request.body.data.attributes.metadata), - request.jwtRequestPayload.userid + request.jwtRequestPayload.userid, ); //Prepare the reply diff --git a/services/auth/recovery.js b/services/auth/recovery.js index 37ae1c3..d9ba9ef 100644 --- a/services/auth/recovery.js +++ b/services/auth/recovery.js @@ -3,7 +3,6 @@ import config from "../../config.js"; import { SMTPClient } from "emailjs"; export const profileRecoveryHandler = async function (request, reply) { - //Create the SMTP client const client = new SMTPClient({ user: config.SMTPUSER, @@ -21,10 +20,12 @@ export const profileRecoveryHandler = async function (request, reply) { //Check if the user exists in the database, before issuing recovery token if (!userObj) { request.log.info( - "User was not found in the Database - Profile Recovery failed" + "User was not found in the Database - Profile Recovery failed", ); // send a request to the client - reply.code(200).send({ statusCode: 200, message: "Recovery email issued" }); + reply + .code(200) + .send({ statusCode: 200, message: "Recovery email issued" }); } //Prepare & send the recovery email @@ -216,8 +217,7 @@ export const profileRecoveryHandler = async function (request, reply) { return { data: { type: "users", - detail: - "Recovery email sent", + detail: "Recovery email sent", }, }; } catch (err) { diff --git a/services/auth/refresh.js b/services/auth/refresh.js index 261903a..bc0eb1d 100644 --- a/services/auth/refresh.js +++ b/services/auth/refresh.js @@ -14,7 +14,7 @@ export const tokenRefreshHandler = async function (request, reply) { request.refreshToken = cookies.userRefreshToken; } else { request.log.info( - "Auth API: The request does not include a refresh token as cookie, refresh failed" + "Auth API: The request does not include a refresh token as cookie, refresh failed", ); throw { statusCode: 400, message: "Refresh Token Failed" }; } @@ -24,7 +24,7 @@ export const tokenRefreshHandler = async function (request, reply) { //Fetch user from Database const stmt = this.db.prepare( - "SELECT uuid, name, email, jwt_id, created_at, updated_at FROM users WHERE jwt_id = ?;" + "SELECT uuid, name, email, jwt_id, created_at, updated_at FROM users WHERE jwt_id = ?;", ); const userObj = await stmt.get(jwtClaims.jti); diff --git a/services/auth/registration.js b/services/auth/registration.js index 3317e66..cf6f406 100644 --- a/services/auth/registration.js +++ b/services/auth/registration.js @@ -8,7 +8,9 @@ export const registrationHandler = async function (request, reply) { try { //Check the request's type attibute is set to users if (request.body.data.type !== "users") { - request.log.info("Auth API: The request's type is not set to Users, registration failed"); + request.log.info( + "Auth API: The request's type is not set to Users, registration failed", + ); throw { statusCode: 400, message: "Invalid Type Attribute" }; } @@ -17,7 +19,9 @@ export const registrationHandler = async function (request, reply) { const requestedAccount = await stmt.get(request.body.data.attributes.email); if (requestedAccount) { - request.log.info("Auth API: User already exists in database, registration failed"); + request.log.info( + "Auth API: User already exists in database, registration failed", + ); throw { statusCode: 400, message: "Registration Failed" }; } @@ -27,7 +31,7 @@ export const registrationHandler = async function (request, reply) { const jwtid = randomUUID(); const registerStmt = this.db.prepare( - "INSERT INTO users (uuid, name, email, password, metadata, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, metadata, appdata, jwt_id, created_at, updated_at;" + "INSERT INTO users (uuid, name, email, password, metadata, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, metadata, appdata, jwt_id, created_at, updated_at;", ); const userObj = registerStmt.get( uuid, @@ -36,7 +40,7 @@ export const registrationHandler = async function (request, reply) { hashpwd, JSON.stringify(request.body.data.attributes.metadata), "1", - jwtid + jwtid, ); //Prepare the reply const userAccessToken = await makeAccesstoken(userObj, this.key); diff --git a/services/webAuthn/loginOptions.js b/services/webAuthn/loginOptions.js index c8345e4..a654420 100644 --- a/services/webAuthn/loginOptions.js +++ b/services/webAuthn/loginOptions.js @@ -23,7 +23,7 @@ export const loginOptionsHandler = async function (request, reply) { //persist the challenge with the associated session id for the verification step in loginVerification.js const stmt = this.db.prepare( - "INSERT INTO storage (sessionID, data) VALUES (?, ?);" + "INSERT INTO storage (sessionID, data) VALUES (?, ?);", ); await stmt.run(cookies.sessionID, generatedOptions.challenge); diff --git a/services/webAuthn/loginVerification.js b/services/webAuthn/loginVerification.js index 1b49615..b7cb6ca 100644 --- a/services/webAuthn/loginVerification.js +++ b/services/webAuthn/loginVerification.js @@ -17,7 +17,7 @@ export const loginVerificationHandler = async function (request, reply) { //retrieve the session's challenge from storage const storageStmt = this.db.prepare( - "SELECT data FROM storage WHERE sessionID = ?;" + "SELECT data FROM storage WHERE sessionID = ?;", ); const sessionChallenge = await storageStmt.get(cookies.sessionID); @@ -26,7 +26,7 @@ export const loginVerificationHandler = async function (request, reply) { //retrieve the user's authenticator const stmt = this.db.prepare( - "SELECT credentialPublicKey, credentialID, counter, transports FROM authenticator INNER JOIN users ON users.authenticator_id = authenticator.id WHERE users.uuid = ?;" + "SELECT credentialPublicKey, credentialID, counter, transports FROM authenticator INNER JOIN users ON users.authenticator_id = authenticator.id WHERE users.uuid = ?;", ); const userAuthenticator = await stmt.get(userID); @@ -48,7 +48,7 @@ export const loginVerificationHandler = async function (request, reply) { //session clean up in the storage const deleteStmt = this.db.prepare( - "DELETE FROM storage WHERE sessionID = ?;" + "DELETE FROM storage WHERE sessionID = ?;", ); await deleteStmt.run(cookies.sessionID); @@ -56,7 +56,7 @@ export const loginVerificationHandler = async function (request, reply) { // Fetch user from database const userStmt = this.db.prepare( - "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM users WHERE uuid = ?;" + "SELECT uuid, name, email, jwt_id, password, active, created_at, updated_at FROM users WHERE uuid = ?;", ); const userObj = await userStmt.get(userID); diff --git a/services/webAuthn/registrationOptions.js b/services/webAuthn/registrationOptions.js index 9f8ed50..d3a36ed 100644 --- a/services/webAuthn/registrationOptions.js +++ b/services/webAuthn/registrationOptions.js @@ -57,7 +57,7 @@ export const registrationOptionsHandler = async function (request, reply) { //create user const registerStmt = this.db.prepare( - "INSERT INTO users (uuid, name, email, password, challenge, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, jwt_id, created_at, updated_at;" + "INSERT INTO users (uuid, name, email, password, challenge, active, jwt_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now')) RETURNING uuid, name, email, jwt_id, created_at, updated_at;", ); const userObj = registerStmt.get( userUUID, @@ -66,7 +66,7 @@ export const registrationOptionsHandler = async function (request, reply) { hashpwd, generatedOptions.challenge, "0", - jwtid + jwtid, ); //send the reply return generatedOptions; diff --git a/services/webAuthn/registrationVerification.js b/services/webAuthn/registrationVerification.js index d63179e..8d4a228 100644 --- a/services/webAuthn/registrationVerification.js +++ b/services/webAuthn/registrationVerification.js @@ -36,17 +36,17 @@ export const registrationVerificationHandler = async function (request, reply) { verification.registrationInfo; const authenticatorStmt = this.db.prepare( - "INSERT INTO Authenticator (credentialID, credentialPublicKey, counter) VALUES (?,?,?) RETURNING *;" + "INSERT INTO Authenticator (credentialID, credentialPublicKey, counter) VALUES (?,?,?) RETURNING *;", ); const authenticatorObj = authenticatorStmt.get( credentialID, credentialPublicKey, - counter + counter, ); //associate the authenticator to the user and activate the account const userStmt = this.db.prepare( - "UPDATE users SET authenticator_id = ?, active = ? WHERE uuid = ? RETURNING uuid, name, email, jwt_id, created_at, updated_at;" + "UPDATE users SET authenticator_id = ?, active = ? WHERE uuid = ? RETURNING uuid, name, email, jwt_id, created_at, updated_at;", ); const userObj = userStmt.get(authenticatorObj.id, "1", requestedAccount); diff --git a/tests/app.test.js b/tests/app.test.js index 3c073b2..a3170a1 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -3,7 +3,11 @@ import buildApp from "../app.js"; import { unlink } from "node:fs/promises"; import { parse } from "cookie"; import { readFile } from "node:fs/promises"; -import { makeAccesstoken, makeRefreshtoken, makeAdminToken } from "../utils/jwt.js"; +import { + makeAccesstoken, + makeRefreshtoken, + makeAdminToken, +} from "../utils/jwt.js"; import * as jose from "jose"; // Setup Test @@ -54,7 +58,9 @@ test.before(async (t) => { const data = await response.json(); t.context.uuid = data.data.id; t.context.jwt = data.data.attributes.access_token; - t.context.refreshToken = parse(response.headers["set-cookie"][0])["userRefreshToken"]; + t.context.refreshToken = parse(response.headers["set-cookie"][0])[ + "userRefreshToken" + ]; t.context.fgp = parse(response.headers["set-cookie"][1])["fgp"]; //create a user to test admin api endpoints with @@ -74,7 +80,8 @@ test.before(async (t) => { //save admin jwt to context t.context.adminJWT = response2.json().data.attributes.access_token; - t.context.adminFgp = parse(response.headers["set-cookie"][1])["fgp"]; + t.context.adminRefresh = response2.json().data.attributes.refresh_token; + // t.context.adminFgp = parse(response.headers["set-cookie"][1])["fgp"]; }); // Cleanup Test @@ -188,6 +195,22 @@ test.serial("Auth Endpoint Test: POST /auth/users/me", async (t) => { } }); +test.serial("Admin Endpoint Test: POST /admin/refesh", async (t) => { + try { + const response = await t.context.app.inject({ + method: "POST", + url: "/v1/admin/refresh", + payload: { + refreshToken: t.context.adminRefresh, + }, + headers: {}, + }); + t.is(response.statusCode, 200, "API - Status Code Incorrect"); + } catch (error) { + console.log(error); + } +}); + test.serial("Admin Endpoint Test: POST /admin/login", async (t) => { try { const response = await t.context.app.inject({ @@ -217,7 +240,6 @@ test.serial("Admin Endpoint Test: GET /admin/users", async (t) => { url: "/v1/admin/users", headers: { Authorization: `Bearer ${t.context.adminJWT}`, - cookie: `${t.context.adminFgp}`, }, }); t.is(response.statusCode, 200, "API - Status Code Incorrect"); @@ -244,7 +266,6 @@ test.serial("Admin Endpoint Test: POST /admin/users", async (t) => { }, headers: { Authorization: `Bearer ${t.context.adminJWT}`, - cookie: `${t.context.adminFgp}`, }, }); t.is(response.statusCode, 201, "API - Status Code Incorrect"); @@ -271,7 +292,6 @@ test.serial("Admin Endpoint Test: PATCH /admin/users/:uuid", async (t) => { }, headers: { Authorization: `Bearer ${t.context.adminJWT}`, - cookie: `${t.context.adminFgp}`, }, }); t.is(response.statusCode, 200, "API - Status Code Incorrect"); @@ -287,7 +307,6 @@ test.serial("Admin Endpoint Test: DELETE /admin/users/:uuid", async (t) => { url: `/v1/admin/users/${t.context.uuid}`, headers: { Authorization: `Bearer ${t.context.adminJWT}`, - cookie: `${t.context.adminFgp}`, }, }); t.is(response.statusCode, 204, "API - Status Code Incorrect"); @@ -307,7 +326,10 @@ test("JWT Test: makeAccesstoken generates a valid JWT token", async (t) => { }; const secretKey = t.context.app.key; - const { token, expiration, userFingerprint } = await makeAccesstoken(userObj, secretKey); + const { token, expiration, userFingerprint } = await makeAccesstoken( + userObj, + secretKey + ); // Fetch the payload const { payload } = await jose.jwtVerify(token, secretKey); @@ -391,7 +413,10 @@ test("JWT Test: makeAdminToken generates a valid admin JWT token", async (t) => const secretKey = t.context.app.key; // Generate an admin token using the function - const { token, expiration, userFingerprint } = await makeAdminToken(userObj, secretKey); + const { token, expiration, userFingerprint } = await makeAdminToken( + userObj, + secretKey + ); // Fetch the payload const { payload } = await jose.jwtVerify(token, secretKey); diff --git a/utils/authenticate.js b/utils/authenticate.js index 62049ab..4022f12 100644 --- a/utils/authenticate.js +++ b/utils/authenticate.js @@ -34,12 +34,9 @@ export const authenticateAdminRequest = async function (request, reply) { : (() => { throw { statusCode: "401", message: "Unauthorized" }; })(); - //extract the specific fingerprint value from the header - const cookies = parse(request.headers.cookie); - const fingerPrint = cookies["Fgp"]; //validate the JWT - const payload = await validateJWT(requestToken, this.key, fingerPrint); + const payload = await validateJWT(requestToken, this.key); //check the jwt payload in the scope claim for admin if (!payload.scope.includes("admin")) { @@ -69,7 +66,7 @@ export const authenticateWebAdminRequest = async function (request, reply) { const payload = await validateJWT( cookies.adminAccessToken, this.key, - fingerPrint + fingerPrint, ); // Check if the payload contains the admin scope diff --git a/utils/jwt.js b/utils/jwt.js index 3152f5f..1724a29 100644 --- a/utils/jwt.js +++ b/utils/jwt.js @@ -21,7 +21,8 @@ export async function makeAccesstoken(userObj, secretKey) { let expirationTime = "1h"; //generate the client context for storing the hash in the jwt claims - const { userFingerprint, userFingerprintHash } = await generateClientContext(); + const { userFingerprint, userFingerprintHash } = + await generateClientContext(); // build the token claims const claims = { @@ -32,11 +33,19 @@ export async function makeAccesstoken(userObj, secretKey) { scope: "user", }; - if (userObj.metadata !== undefined && userObj.metadata !== null && userObj.metadata !== "{}") { + if ( + userObj.metadata !== undefined && + userObj.metadata !== null && + userObj.metadata !== "{}" + ) { claims.metadata = JSON.parse(userObj.metadata); } - if (userObj.appdata !== undefined && userObj.appdata !== null && userObj.appdata !== "{}") { + if ( + userObj.appdata !== undefined && + userObj.appdata !== null && + userObj.appdata !== "{}" + ) { claims.app = JSON.parse(userObj.appdata); } @@ -59,7 +68,11 @@ export async function makeAccesstoken(userObj, secretKey) { } //https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-05#section-8 -export async function makeRefreshtoken(userObj, secretKey, { recoveryToken = false } = {}) { +export async function makeRefreshtoken( + userObj, + secretKey, + { recoveryToken = false } = {}, +) { try { // set default expiration time of the jwt token let expirationTime = "7d"; @@ -74,11 +87,19 @@ export async function makeRefreshtoken(userObj, secretKey, { recoveryToken = fal email: userObj.email, }; - if (userObj.metadata !== undefined && userObj.metadata !== null && userObj.metadata !== "{}") { + if ( + userObj.metadata !== undefined && + userObj.metadata !== null && + userObj.metadata !== "{}" + ) { claims.metadata = JSON.parse(userObj.metadata); } - if (userObj.appdata !== undefined && userObj.appdata !== null && userObj.appdata !== "{}") { + if ( + userObj.appdata !== undefined && + userObj.appdata !== null && + userObj.appdata !== "{}" + ) { claims.app = JSON.parse(userObj.appdata); } @@ -87,7 +108,7 @@ export async function makeRefreshtoken(userObj, secretKey, { recoveryToken = fal const jwtid = randomUUID(); const stmt = db.prepare( - "UPDATE users SET jwt_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ?;" + "UPDATE users SET jwt_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ?;", ); stmt.run(jwtid, userObj.uuid); @@ -110,10 +131,11 @@ export async function makeRefreshtoken(userObj, secretKey, { recoveryToken = fal export async function makeAdminToken(userObj, secretKey) { try { // set default expiration time of the jwt token - let expirationTime = "2h"; + let expirationTime = "1h"; //generate the client context for storing the hash in the jwt claims - const { userFingerprint, userFingerprintHash } = await generateClientContext(); + const { userFingerprint, userFingerprintHash } = + await generateClientContext(); const claims = { userid: userObj.uuid, @@ -141,6 +163,50 @@ export async function makeAdminToken(userObj, secretKey) { } } +export async function makeAdminRefreshtoken(adminObj, secretKey) { + try { + // Set default expiration time of the JWT token + let expirationTime = "7d"; + + // Define the token claims for the admin refresh token + const claims = { + userid: adminObj.uuid, + name: adminObj.name, + email: adminObj.email, + userFingerprint: null, + scope: "admin", + }; + + // Check if adminObj has metadata and appdata properties and add them to claims as needed + + const db = new Database(config.DBPATH); + + // Generate a random UUID for the token + const jwtid = randomUUID(); + + // Update the admin table with the new JWT ID and update timestamp + const stmt = db.prepare( + "UPDATE admin SET jwt_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE uuid = ?;", + ); + stmt.run(jwtid, adminObj.uuid); + + // Create and sign the JWT token + const jwt = await new jose.SignJWT(claims) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt() + .setExpirationTime(expirationTime) + .setJti(jwtid) + .sign(secretKey); + + // Verify the token and retrieve its payload + const { payload } = await jose.jwtVerify(jwt, secretKey); + + return { token: jwt, expiration: payload.exp }; + } catch (error) { + throw { statusCode: 500, message: "Server Error" }; + } +} + // Validates a JWT token export async function validateJWT(jwt, secretKey, fingerprint = null) { try { @@ -152,7 +218,10 @@ export async function validateJWT(jwt, secretKey, fingerprint = null) { return payload; } - const validUserContext = await verifyValueWithHash(fingerprint, payload.userFingerprint); + const validUserContext = await verifyValueWithHash( + fingerprint, + payload.userFingerprint, + ); if (!validUserContext) { throw { statusCode: 500, message: "Server Error" };