Unverified Commit a1815797 authored by NGPixel's avatar NGPixel

feat: turn off 2FA function + switch to pnpm + update dependencies

parent 7f9c3511
......@@ -51,6 +51,9 @@ RUN apt-get update \
# Clean up
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /root/.gnupg /tmp/library-scripts
# Enable PNPM
RUN sudo corepack enable \
&& corepack prepare pnpm@latest --activate
EXPOSE 3000
......
......@@ -9,16 +9,14 @@ git config oh-my-zsh.hide-info 1
echo "Waiting for DB container to come online..."
/usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
npm install -g npm-check-updates
echo "Installing dependencies..."
cd server
npm install
pnpm install
cd ../ux
npm install
pnpm install
cd ../blocks
npm install
npm run build
pnpm install
pnpm build
cd ..
echo "Ready!"
......@@ -15,6 +15,7 @@ npm/node_modules
.node_repl_history
npm-debug.log*
/.yarn
/.pnpm-store
# Generated assets
/assets
......
......@@ -62,11 +62,11 @@ The current stable release (2.x) is available at https://js.wiki
1. Two terminals will launch in split-screen mode at the bottom of the screen. **Server** on the left and **UX** on the right.
1. In the right-side terminal (UX), run the command:
```sh
npm run build
pnpm build
```
1. In the left-side terminal (Server), run the command:
```sh
npm run start
pnpm start
```
1. Open your browser to `http://localhost:3000`
1. Login using the default administrator user:
......@@ -80,7 +80,7 @@ The current stable release (2.x) is available at https://js.wiki
From the left-side terminal (Server), run the command:
```sh
npm run dev
pnpm dev
```
This will launch the server and automatically restart upon modification of any server files.
......@@ -94,7 +94,7 @@ Only precompiled client assets are served in this mode. See the sections below o
If you wish to modify any frontend content (under `/ux`), you need to start the Quasar Dev Server in the right-side terminal (UX):
```sh
npm run dev
pnpm dev
```
You can then access the site at `http://localhost:3001`. Notice the port being `3001` rather than `3000`. The app runs in a SPA (single-page application) mode and automatically hot-reload any modified component. Any requests made to the `/graphql` endpoint are automatically forwarded to the server running on port `3000`, which is why both must be running at the same time.
......@@ -117,8 +117,9 @@ The server **dev** should already be available under **Servers**. If that's not
### Requirements
- PostgreSQL **11** or later *(**16** or later recommended)*
- Node.js **18.x** or later
- PostgreSQL **11** or later
- [pnpm](https://pnpm.io/installation#using-corepack)
### Usage
......@@ -128,10 +129,13 @@ The server **dev** should already be available under **Servers**. If that's not
1. Run the following commands to install dependencies and generate the client assets:
```sh
cd server
npm install
pnpm install
cd ../ux
npm install
npm run build
pnpm install
pnpm build
cd ../blocks
pnpm install
pnpm build
cd ..
```
1. Run this command to start the server:
......
Binary files a/blocks/package-lock.json and /dev/null differ
......@@ -11,14 +11,14 @@
"author": "Nicolas Giard",
"license": "AGPL-3.0",
"dependencies": {
"lit": "^2.8.0"
"lit": "2.8.0"
},
"devDependencies": {
"@rollup/plugin-graphql": "^2.0.3",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-terser": "^0.4.3",
"glob": "^10.3.4",
"rollup": "^2.79.1",
"rollup-plugin-summary": "^2.0.0"
"@rollup/plugin-graphql": "2.0.4",
"@rollup/plugin-node-resolve": "15.2.2",
"@rollup/plugin-terser": "0.4.4",
"glob": "10.3.10",
"rollup": "3.29.4",
"rollup-plugin-summary": "2.0.0"
}
}
extends:
- requarks
- plugin:vue/strongly-recommended
env:
node: true
jest: true
parserOptions:
parser: babel-eslint
ecmaVersion: 2020
......@@ -16,9 +14,8 @@ globals:
ignorePatterns:
- '**/node_modules/**'
- '**/*.min.js'
- 'assets/**'
- 'coverage/**'
- 'repo/**'
- 'data/**'
- 'logs/**'
- 'server/locales/**'
- 'locales/**'
......@@ -123,6 +123,48 @@ export default {
}
},
/**
* Deactivate 2FA
*/
async deactivateTFA (obj, args, context) {
try {
const userId = context.req.user?.id
if (!userId) {
throw new Error('ERR_USER_NOT_AUTHENTICATED')
}
const usr = await WIKI.db.users.query().findById(userId)
if (!usr) {
throw new Error('ERR_INVALID_USER')
}
const str = WIKI.auth.strategies[args.strategyId]
if (!str) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId]) {
throw new Error('ERR_INVALID_STRATEGY')
}
if (!usr.auth[args.strategyId].tfaIsActive) {
throw new Error('ERR_TFA_NOT_ACTIVE')
}
usr.auth[args.strategyId].tfaIsActive = false
usr.auth[args.strategyId].tfaSecret = null
await usr.$query().patch({
auth: usr.auth
})
return {
operation: generateSuccess('TFA deactivated successfully.')
}
} catch (err) {
return generateError(err)
}
},
/**
* Perform Password Change
*/
async changePassword (obj, args, context) {
......
......@@ -412,8 +412,8 @@ export default {
strategyIcon: authModule.icon,
config: authStrategy.module === 'local' ? {
isPasswordSet: value.password?.length > 0,
isTfaSetup: value.tfaSecret?.length > 0,
isTfaRequired: value.tfaRequired ?? false,
isTfaSetup: value.tfaIsActive && value.tfaSecret?.length > 0,
isTfaRequired: (value.tfaRequired || authStrategy.config.enforceTfa) ?? false,
mustChangePwd: value.mustChangePwd ?? false,
restrictLogin: value.restrictLogin ?? false
} : value
......
......@@ -41,6 +41,10 @@ extend type Mutation {
setup: Boolean
): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
deactivateTFA(
strategyId: UUID!
): DefaultResponse
changePassword(
continuationToken: String
currentPassword: String
......
......@@ -1131,6 +1131,7 @@
"auth.changePwd.subtitle": "Choose a new password",
"auth.changePwd.success": "Password updated successfully.",
"auth.enterCredentials": "Enter your credentials",
"auth.errors.fields": "One or more fields are invalid.",
"auth.errors.forgotPassword": "Missing or invalid email address.",
"auth.errors.invalidEmail": "Email is invalid.",
"auth.errors.invalidLogin": "Invalid Login",
......@@ -1152,7 +1153,6 @@
"auth.errors.tooManyAttempts": "Too many attempts!",
"auth.errors.tooManyAttemptsMsg": "You've made too many failed attempts in a short period of time, please try again {time}.",
"auth.errors.userNotFound": "User not found",
"auth.errors.fields": "One or more fields are invalid.",
"auth.fields.email": "Email Address",
"auth.fields.emailUser": "Email / Username",
"auth.fields.name": "Name",
......@@ -1748,6 +1748,9 @@
"profile.auth": "Authentication",
"profile.authChangePassword": "Change Password",
"profile.authDisableTfa": "Turn Off 2FA",
"profile.authDisableTfaConfirm": "Are you sure you want to disable Two Factor Authentication?",
"profile.authDisableTfaFailed": "Failed to turn off 2FA.",
"profile.authDisableTfaSuccess": "2FA turned off successfully.",
"profile.authInfo": "Your account is associated with the following authentication methods:",
"profile.authLoadingFailed": "Failed to load authentication methods.",
"profile.authModifyTfa": "Modify 2FA",
......
Binary files a/server/package-lock.json and /dev/null differ
......@@ -36,18 +36,18 @@
"node": ">=18.0"
},
"dependencies": {
"@apollo/server": "4.9.3",
"@apollo/server": "4.9.4",
"@azure/storage-blob": "12.16.0",
"@exlinc/keycloak-passport": "1.0.2",
"@graphql-tools/schema": "10.0.0",
"@graphql-tools/utils": "10.0.1",
"@graphql-tools/utils": "10.0.6",
"@joplin/turndown-plugin-gfm": "1.0.50",
"@root/csr": "0.8.1",
"@root/keypairs": "0.10.3",
"@root/pem": "1.0.4",
"acme": "3.0.3",
"akismet-api": "6.0.0",
"aws-sdk": "2.1463.0",
"aws-sdk": "2.1472.0",
"bcryptjs": "2.4.3",
"chalk": "5.3.0",
"cheerio": "1.0.0-rc.12",
......@@ -64,8 +64,8 @@
"custom-error-instance": "2.1.2",
"dependency-graph": "0.11.0",
"diff": "5.1.0",
"diff2html": "3.4.43",
"dompurify": "3.0.5",
"diff2html": "3.4.45",
"dompurify": "3.0.6",
"dotize": "0.3.0",
"emoji-regex": "10.2.1",
"eventemitter2": "6.4.9",
......@@ -73,7 +73,7 @@
"express-brute": "1.0.1",
"express-session": "1.17.3",
"file-type": "18.5.0",
"filesize": "10.0.12",
"filesize": "10.1.0",
"fs-extra": "11.1.1",
"getos": "3.2.1",
"graphql": "16.8.1",
......@@ -91,13 +91,13 @@
"js-yaml": "4.1.0",
"jsdom": "22.1.0",
"jsonwebtoken": "9.0.2",
"katex": "0.16.8",
"katex": "0.16.9",
"klaw": "4.1.0",
"knex": "2.5.1",
"knex": "3.0.1",
"lodash": "4.17.21",
"lodash-es": "4.17.21",
"luxon": "3.4.3",
"markdown-it": "13.0.1",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "4.1.6",
"markdown-it-decorate": "1.2.2",
......@@ -114,13 +114,13 @@
"mathjax": "3.2.2",
"mime-types": "2.1.35",
"ms": "2.1.3",
"multer": "1.4.4",
"multer": "1.4.5-lts.1",
"nanoid": "5.0.1",
"node-2fa": "2.0.3",
"node-cache": "5.1.2",
"nodemailer": "6.9.5",
"objection": "3.1.1",
"octokit": "3.1.0",
"objection": "3.1.2",
"octokit": "3.1.1",
"passport": "0.6.0",
"passport-auth0": "1.4.3",
"passport-azure-ad": "4.3.5",
......@@ -138,7 +138,7 @@
"passport-oauth2": "1.7.0",
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.1.1",
"passport-saml": "3.2.1",
"passport-saml": "3.2.4",
"passport-slack-oauth2": "1.2.0",
"passport-twitch-strategy": "2.2.0",
"pem-jwk": "2.0.0",
......@@ -147,9 +147,9 @@
"pg-pubsub": "0.8.1",
"pg-query-stream": "4.5.3",
"pg-tsquery": "8.4.1",
"poolifier": "2.7.1",
"poolifier": "2.7.5",
"punycode": "2.3.0",
"puppeteer-core": "21.3.4",
"puppeteer-core": "21.3.8",
"qr-image": "3.2.0",
"remove-markdown": "0.5.0",
"request": "2.88.2",
......@@ -160,7 +160,7 @@
"semver": "7.5.4",
"serve-favicon": "2.5.0",
"sharp": "0.32.6",
"simple-git": "3.19.1",
"simple-git": "3.20.0",
"socket.io": "4.7.2",
"striptags": "3.2.0",
"tar-fs": "3.0.4",
......@@ -173,13 +173,13 @@
"yargs": "17.7.2"
},
"devDependencies": {
"eslint": "8.50.0",
"eslint": "8.51.0",
"eslint-config-requarks": "1.0.7",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-standard": "4.1.0",
"eslint-plugin-standard": "5.0.0",
"nodemon": "3.0.1"
},
"overrides": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Binary files a/ux/package-lock.json and /dev/null differ
......@@ -13,10 +13,10 @@
"ncu-u": "ncu -u -x codemirror,codemirror-asciidoc"
},
"dependencies": {
"@apollo/client": "3.8.4",
"@apollo/client": "3.8.5",
"@lezer/common": "1.1.0",
"@mdi/font": "7.2.96",
"@quasar/extras": "1.16.6",
"@mdi/font": "7.3.67",
"@quasar/extras": "1.16.7",
"@tiptap/core": "2.1.11",
"@tiptap/extension-code-block": "2.1.11",
"@tiptap/extension-code-block-lowlight": "2.1.11",
......@@ -48,7 +48,7 @@
"codemirror": "5.65.11",
"codemirror-asciidoc": "1.0.4",
"dependency-graph": "0.11.0",
"filesize": "10.0.12",
"filesize": "10.1.0",
"filesize-parser": "1.5.0",
"fuse.js": "6.6.2",
"graphql": "16.6.0",
......@@ -56,11 +56,11 @@
"highlight.js": "11.8.0",
"js-cookie": "3.0.5",
"jwt-decode": "3.1.2",
"katex": "0.16.8",
"katex": "0.16.9",
"lodash-es": "4.17.21",
"lowlight": "3.0.0",
"luxon": "3.4.3",
"markdown-it": "13.0.1",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "4.1.6",
"markdown-it-decorate": "1.2.2",
......@@ -75,7 +75,7 @@
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mitt": "3.0.1",
"monaco-editor": "0.43.0",
"monaco-editor": "0.44.0",
"pako": "2.1.0",
"pinia": "2.1.6",
"prosemirror-commands": "1.5.2",
......@@ -84,8 +84,8 @@
"prosemirror-model": "1.19.3",
"prosemirror-schema-list": "1.3.0",
"prosemirror-state": "1.4.3",
"prosemirror-transform": "1.7.5",
"prosemirror-view": "1.31.8",
"prosemirror-transform": "1.8.0",
"prosemirror-view": "1.32.0",
"pug": "3.0.2",
"quasar": "2.12.7",
"slugify": "1.6.6",
......@@ -95,9 +95,9 @@
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
"uuid": "9.0.1",
"v-network-graph": "0.9.8",
"v-network-graph": "0.9.10",
"vue": "3.3.4",
"vue-i18n": "9.4.1",
"vue-i18n": "9.5.0",
"vue-router": "4.2.5",
"vue3-otp-input": "0.4.1",
"vuedraggable": "4.1.0",
......@@ -105,13 +105,13 @@
"zxcvbn": "4.4.2"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "1.2.0",
"@intlify/unplugin-vue-i18n": "1.4.0",
"@quasar/app-vite": "1.6.2",
"@types/lodash": "4.14.199",
"@volar/vue-language-plugin-pug": "1.6.5",
"@vue/language-plugin-pug": "1.8.16",
"autoprefixer": "10.4.16",
"browserlist": "latest",
"eslint": "8.50.0",
"eslint": "8.51.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-n": "16.1.0",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<template lang="pug">
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 650px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
span {{t(`admin.users.changePassword`)}}
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
q-item
blueprint-icon(icon='lock')
q-item-section
q-input(
outlined
v-model='state.currentPassword'
dense
:rules='currentPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.currentPassword`)'
:aria-label='t(`auth.changePwd.currentPassword`)'
lazy-rules='ondemand'
autofocus
)
q-item
blueprint-icon(icon='password')
q-item-section
q-input(
outlined
v-model='state.newPassword'
dense
:rules='newPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.newPassword`)'
:aria-label='t(`auth.changePwd.newPassword`)'
lazy-rules='ondemand'
autofocus
)
template(#append)
.flex.items-center
q-badge(
:color='passwordStrength.color'
:label='passwordStrength.label'
)
q-separator.q-mx-sm(vertical)
q-btn(
flat
dense
padding='none xs'
color='brown'
@click='randomizePassword'
)
q-icon(name='las la-dice-d6')
.q-pl-xs.text-caption: strong Generate
q-item
blueprint-icon(icon='good-pincode')
q-item-section
q-input(
outlined
v-model='state.verifyPassword'
dense
:rules='verifyPasswordValidation'
hide-bottom-space
:label='t(`auth.changePwd.newPasswordVerify`)'
:aria-label='t(`auth.changePwd.newPasswordVerify`)'
lazy-rules='ondemand'
autofocus
)
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='t(`common.actions.update`)'
color='primary'
padding='xs md'
@click='save'
:loading='state.isLoading'
)
</template>
<script setup>
import gql from 'graphql-tag'
import zxcvbn from 'zxcvbn'
import { sampleSize } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, reactive, ref } from 'vue'
import { useSiteStore } from 'src/stores/site'
// PROPS
const props = defineProps({
strategyId: {
type: String,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
currentPassword: '',
newPassword: '',
verifyPassword: '',
isLoading: false
})
// REFS
const changeUserPwdForm = ref(null)
// COMPUTED
const passwordStrength = computed(() => {
if (state.newPassword.length < 8) {
return {
color: 'negative',
label: t('admin.users.pwdStrengthWeak')
}
} else {
switch (zxcvbn(state.newPassword).score) {
case 1:
return {
color: 'deep-orange-7',
label: t('admin.users.pwdStrengthPoor')
}
case 2:
return {
color: 'purple-7',
label: t('admin.users.pwdStrengthMedium')
}
case 3:
return {
color: 'blue-7',
label: t('admin.users.pwdStrengthGood')
}
case 4:
return {
color: 'green-7',
label: t('admin.users.pwdStrengthStrong')
}
default:
return {
color: 'negative',
label: t('admin.users.pwdStrengthWeak')
}
}
}
})
// VALIDATION RULES
const currentPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingPassword')
]
const newPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingPassword'),
val => val.length >= 8 || t('auth.errors.passwordTooShort')
]
const verifyPasswordValidation = [
val => val.length > 0 || t('auth.errors.missingVerifyPassword'),
val => val === state.newPassword || t('auth.errors.passwordsNotMatch')
]
// METHODS
function randomizePassword () {
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
state.newPassword = sampleSize(pwdChars, 16).join('')
}
async function save () {
state.isLoading = true
try {
const isFormValid = await changeUserPwdForm.value.validate(true)
if (!isFormValid) {
throw new Error(t('auth.errors.fields'))
}
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation changePwd (
$currentPassword: String
$newPassword: String!
$strategyId: UUID!
$siteId: UUID!
) {
changePassword (
currentPassword: $currentPassword
newPassword: $newPassword
strategyId: $strategyId
siteId: $siteId
) {
operation {
succeeded
message
}
}
}
`,
variables: {
currentPassword: state.currentPassword,
newPassword: state.newPassword,
strategyId: props.strategyId,
siteId: siteStore.id
}
})
if (resp?.data?.changePassword?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('auth.changePwd.success')
})
onDialogOK()
} else {
throw new Error(resp?.data?.changePassword?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>
......@@ -27,7 +27,8 @@ q-page.q-py-md(:style-fn='pageStyle')
unelevated
:label='t(`profile.authDisableTfa`)'
color='negative'
@click=''
@click='disableTfa(auth.authId)'
:disable='auth.config.isTfaRequired'
)
q-item-section(v-else, side)
q-btn(
......@@ -35,7 +36,7 @@ q-page.q-py-md(:style-fn='pageStyle')
unelevated
:label='t(`profile.authSetTfa`)'
color='primary'
@click=''
@click='setupTfa(auth.authId)'
)
q-item-section(side)
q-btn(
......@@ -58,6 +59,7 @@ import { onMounted, reactive } from 'vue'
import { useUserStore } from 'src/stores/user'
import ChangePwdDialog from 'src/components/ChangePwdDialog.vue'
import SetupTfaDialog from 'src/components/SetupTfaDialog.vue'
// QUASAR
......@@ -139,6 +141,62 @@ function changePassword (strategyId) {
})
}
function disableTfa (strategyId) {
$q.dialog({
title: t('common.actions.confirm'),
message: t('profile.authDisableTfaConfirm'),
cancel: true
}).onOk(async () => {
$q.loading.show()
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deactivateTfa (
$strategyId: UUID!
) {
deactivateTFA(
strategyId: $strategyId
) {
operation {
succeeded
message
}
}
}
`,
variables: {
strategyId
}
})
if (resp?.data?.deactivateTFA?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('profile.authDisableTfaSuccess')
})
} else {
throw new Error(resp?.data?.deactivateTFA?.operation?.message)
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('profile.authDisableTfaFailed'),
caption: err.message ?? 'An unexpected error occured.'
})
}
await fetchAuthMethods()
$q.loading.hide()
})
}
function setupTfa (strategyId) {
// $q.dialog({
// component: SetupTfaDialog,
// componentProps: {
// strategyId
// }
// })
}
// MOUNTED
onMounted(() => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment