diff --git a/.editorconfig b/.editorconfig
index 49af1a19..8f066db4 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,3 +9,7 @@ insert_final_newline = true
[*.{jade,pug,md}]
trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
+indent_size = 4
diff --git a/Makefile b/Makefile
index b18fecc0..2ebf8e2e 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,7 @@ docker-dev-down: ## Shutdown dockerized dev environment
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down
docker-dev-rebuild: ## Rebuild dockerized dev image
+ rm -rf ./node_modules
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm
docker-build: ## Run assets generation build in docker
diff --git a/client/client-app.js b/client/client-app.js
index 661c57e4..30f1f120 100644
--- a/client/client-app.js
+++ b/client/client-app.js
@@ -163,10 +163,12 @@ Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './component
Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
+Vue.component('loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
Vue.component('profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
+Vue.component('register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
Vue.component('v-card-chin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
Vue.component('nav-footer', () => import(/* webpackChunkName: "theme-page" */ './themes/' + process.env.CURRENT_THEME + '/components/nav-footer.vue'))
diff --git a/client/components/admin/admin-auth.vue b/client/components/admin/admin-auth.vue
index 65c2cd43..61aba5a9 100644
--- a/client/components/admin/admin-auth.vue
+++ b/client/components/admin/admin-auth.vue
@@ -151,6 +151,18 @@
multiple
chips
)
+ template(v-if='strategy.key === `local`')
+ v-divider.mt-3
+ v-subheader.pl-0 Security
+ .pr-3
+ v-switch.ml-3(
+ :disabled='true'
+ v-model='strategy.recaptcha'
+ label='Use reCAPTCHA by Google'
+ color='primary'
+ hint='Protects against spam robots and malicious registrations.'
+ persistent-hint
+ )
+
+
diff --git a/client/components/common/password-strength.vue b/client/components/common/password-strength.vue
new file mode 100644
index 00000000..0203ab18
--- /dev/null
+++ b/client/components/common/password-strength.vue
@@ -0,0 +1,79 @@
+
+ .password-strength
+ v-progress-linear(
+ :color='passwordStrengthColor'
+ v-model='passwordStrength'
+ height='2'
+ )
+ .caption(v-if='!hideText', :class='passwordStrengthColor + "--text"') {{passwordStrengthText}}
+
+
+
+
+
diff --git a/client/components/editor.vue b/client/components/editor.vue
index 443d15a7..d7c4efb0 100644
--- a/client/components/editor.vue
+++ b/client/components/editor.vue
@@ -30,16 +30,6 @@
v-content
component(:is='currentEditor')
editor-modal-properties(v-model='dialogProps')
- v-dialog(v-model='dialogProgress', persistent, max-width='350')
- v-card(color='blue darken-3', dark)
- v-card-text.text-xs-center.py-4
- atom-spinner.is-inline(
- :animation-duration='1000'
- :size='60'
- color='#FFF'
- )
- .subheading {{ $t('editor:save.processing') }}
- .caption.blue--text.text--lighten-3 {{ $t('editor:save.pleaseWait') }}
v-dialog(v-model='dialogEditorSelector', persistent, max-width='700')
v-card.radius-7(color='blue darken-3', dark)
v-card-text.text-xs-center.py-4
@@ -88,6 +78,7 @@
.caption.grey--text.text--darken-1 Drag-n-drop
.caption.blue--text.text--lighten-2 This cannot be changed once the page is created.
+ loader(v-model='dialogProgress', :title='$t(`editor:save.processing`)', :subtitle='$t(`editor:save.pleaseWait`)')
v-snackbar(
:color='notification.style'
bottom,
diff --git a/client/components/login.vue b/client/components/login.vue
index a65b63d7..24ffa9a3 100644
--- a/client/components/login.vue
+++ b/client/components/login.vue
@@ -15,7 +15,7 @@
v-toolbar(color='primary', flat, dense, dark)
v-spacer
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
- .subheading(v-else-if='selectedStrategy.key !== "local"') Login using {{ selectedStrategy.title }}
+ .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title }) }}
.subheading(v-else) {{ $t('auth:loginRequired') }}
v-spacer
v-card-text.text-xs-center
@@ -80,12 +80,12 @@
v-spacer
v-card-actions.pb-3(v-if='selectedStrategy.key === "local"')
v-spacer
- a.caption(href='') Forgot your password?
+ a.caption(href='') {{ $t('auth:forgotPasswordLink') }}
v-spacer
template(v-if='isSocialShown')
v-divider
v-card-text.grey.lighten-4.text-xs-center
- .pb-2.body-2.text-xs-center.grey--text.text--darken-2 or login using...
+ .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
.social-login-btn.mr-2(
slot='activator'
@@ -99,8 +99,11 @@
v-divider
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
v-spacer
- .caption Don't have an account yet? #[a.caption(href='') Create an account]
+ i18next.caption(path='auth:switchToRegister.text', tag='div')
+ a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
v-spacer
+
+ loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
nav-footer(color='grey darken-4')
@@ -128,6 +131,8 @@ export default {
securityCode: '',
loginToken: '',
isLoading: false,
+ loaderColor: 'grey darken-4',
+ loaderTitle: 'Working...',
isShown: false
}
},
@@ -173,18 +178,20 @@ export default {
if (this.username.length < 2) {
this.$store.commit('showNotification', {
style: 'red',
- message: 'Enter a valid email / username.',
+ message: this.$t('auth:invalidEmailUsername'),
icon: 'warning'
})
this.$refs.iptEmail.focus()
} else if (this.password.length < 2) {
this.$store.commit('showNotification', {
style: 'red',
- message: 'Enter a valid password.',
+ message: this.$t('auth:invalidPassword'),
icon: 'warning'
})
this.$refs.iptPassword.focus()
} else {
+ this.loaderColor = 'grey darken-4'
+ this.loaderTitle = this.$t('auth:signingIn')
this.isLoading = true
try {
let resp = await this.$apollo.mutate({
@@ -205,23 +212,20 @@ export default {
this.$nextTick(() => {
this.$refs.iptTFA.focus()
})
+ this.isLoading = false
} else {
- this.$store.commit('showNotification', {
- message: 'Login Successful! Redirecting...',
- style: 'success',
- icon: 'check'
- })
+ this.loaderColor = 'green darken-1'
+ this.loaderTitle = this.$t('auth:loginSuccess')
Cookies.set('jwt', respObj.jwt, { expires: 365 })
_.delay(() => {
window.location.replace('/') // TEMPORARY - USE RETURNURL
}, 1000)
}
- this.isLoading = false
} else {
throw new Error(respObj.responseResult.message)
}
} else {
- throw new Error('Authentication is unavailable.')
+ throw new Error(this.$t('auth:genericError'))
}
} catch (err) {
console.error(err)
@@ -270,7 +274,7 @@ export default {
throw new Error(respObj.responseResult.message)
}
} else {
- throw new Error('Authentication is unavailable.')
+ throw new Error(this.$t('auth:genericError'))
}
}).catch(err => {
console.error(err)
@@ -289,7 +293,6 @@ export default {
query: strategiesQuery,
update: (data) => data.authentication.strategies,
watchLoading (isLoading) {
- this.isLoading = isLoading
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
}
}
diff --git a/client/components/register.vue b/client/components/register.vue
new file mode 100644
index 00000000..d253220c
--- /dev/null
+++ b/client/components/register.vue
@@ -0,0 +1,303 @@
+
+ v-app
+ .register
+ v-container(grid-list-lg)
+ v-layout(row, wrap)
+ v-flex(
+ xs12
+ offset-sm1, sm10
+ offset-md2, md8
+ offset-lg3, lg6
+ offset-xl4, xl4
+ )
+ transition(name='zoom')
+ v-card.elevation-5.md2(v-show='isShown')
+ v-toolbar(color='indigo', flat, dense, dark)
+ v-spacer
+ .subheading {{ $t('auth:registerTitle') }}
+ v-spacer
+ v-card-text.text-xs-center
+ h1.display-1.indigo--text.py-2 {{ siteTitle }}
+ .body-2 {{ $t('auth:registerSubTitle') }}
+ v-text-field.md2.mt-3(
+ solo
+ flat
+ prepend-icon='email'
+ background-color='grey lighten-4'
+ hide-details
+ ref='iptEmail'
+ v-model='email'
+ :placeholder='$t("auth:fields.email")'
+ color='indigo'
+ )
+ v-text-field.md2.mt-2(
+ solo
+ flat
+ prepend-icon='vpn_key'
+ background-color='grey lighten-4'
+ ref='iptPassword'
+ v-model='password'
+ :append-icon='hidePassword ? "visibility" : "visibility_off"'
+ @click:append='() => (hidePassword = !hidePassword)'
+ :type='hidePassword ? "password" : "text"'
+ :placeholder='$t("auth:fields.password")'
+ color='indigo'
+ loading
+ )
+ password-strength(slot='progress', v-model='password')
+ v-text-field.md2.mt-2(
+ solo
+ flat
+ prepend-icon='vpn_key'
+ background-color='grey lighten-4'
+ hide-details
+ ref='iptVerifyPassword'
+ v-model='verifyPassword'
+ @click:append='() => (hidePassword = !hidePassword)'
+ type='password'
+ :placeholder='$t("auth:fields.verifyPassword")'
+ color='indigo'
+ )
+ v-text-field.md2.mt-2(
+ solo
+ flat
+ prepend-icon='person'
+ background-color='grey lighten-4'
+ hide-details
+ ref='iptName'
+ v-model='name'
+ :placeholder='$t("auth:fields.name")'
+ @keyup.enter='register'
+ color='indigo'
+ )
+ v-card-actions.pb-4
+ v-spacer
+ v-btn.md2(
+ block
+ large
+ dark
+ color='indigo'
+ @click='register'
+ round
+ :loading='isLoading'
+ ) {{ $t('auth:actions.register') }}
+ v-spacer
+ v-divider
+ v-card-actions.py-3.grey.lighten-4
+ v-spacer
+ i18next.caption(path='auth:switchToLogin.text', tag='div')
+ a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}
+ v-spacer
+
+ loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
+ nav-footer(color='grey darken-4', dark-color='grey darken-4')
+
+
+
+
+
diff --git a/client/graph/register/register-mutation-create.gql b/client/graph/register/register-mutation-create.gql
new file mode 100644
index 00000000..16cc9836
--- /dev/null
+++ b/client/graph/register/register-mutation-create.gql
@@ -0,0 +1,13 @@
+mutation($email: String!, $password: String!, $name: String!) {
+ authentication {
+ register(email: $email, password: $password, name: $name) {
+ responseResult {
+ succeeded
+ errorCode
+ slug
+ message
+ }
+ jwt
+ }
+ }
+}
diff --git a/client/static/img/icon-browse.png b/client/static/img/icon-browse.png
deleted file mode 100644
index 60a11199..00000000
Binary files a/client/static/img/icon-browse.png and /dev/null differ
diff --git a/client/static/img/icon-people.png b/client/static/img/icon-people.png
deleted file mode 100644
index 4c35b7d0..00000000
Binary files a/client/static/img/icon-people.png and /dev/null differ
diff --git a/client/static/img/icon-unlock.png b/client/static/img/icon-unlock.png
deleted file mode 100644
index 7fbd96b1..00000000
Binary files a/client/static/img/icon-unlock.png and /dev/null differ
diff --git a/client/static/svg/logo-icons8.svg b/client/static/svg/logo-icons8.svg
new file mode 100644
index 00000000..e9a6035e
--- /dev/null
+++ b/client/static/svg/logo-icons8.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/dev/docker/docker-compose.yml b/dev/docker/docker-compose.yml
index 86c5e167..72f541b8 100644
--- a/dev/docker/docker-compose.yml
+++ b/dev/docker/docker-compose.yml
@@ -30,8 +30,6 @@ services:
adminer:
image: adminer:latest
- environment:
- ADMINER_DESIGN: pappu687
logging:
driver: "none"
networks:
diff --git a/package.json b/package.json
index dbb526fe..13c5051e 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"docker:dev:up": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . up -d && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki yarn dev",
"docker:dev:down": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down",
- "docker:dev:rebuild": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm",
+ "docker:dev:rebuild": "rmdir node_modules /s /q && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm",
"docker:build": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down"
},
"bin": {
@@ -59,6 +59,7 @@
"connect-redis": "3.4.0",
"cookie-parser": "1.4.3",
"cors": "2.8.5",
+ "custom-error-instance": "2.1.1",
"dependency-graph": "0.7.2",
"diff": "3.5.0",
"diff2html": "2.5.0",
@@ -154,6 +155,7 @@
"subscriptions-transport-ws": "0.9.15",
"uslug": "1.0.4",
"uuid": "3.3.2",
+ "validate.js": "0.12.0",
"validator": "10.9.0",
"validator-as-promised": "1.0.2",
"winston": "3.1.0",
@@ -284,7 +286,8 @@
"webpack-subresource-integrity": "1.3.0",
"whatwg-fetch": "3.0.0",
"write-file-webpack-plugin": "4.4.1",
- "xterm": "3.8.0"
+ "xterm": "3.8.0",
+ "zxcvbn": "4.4.2"
},
"browserslist": [
"> 1%",
diff --git a/server/controllers/auth.js b/server/controllers/auth.js
index d5b7e110..fe20b107 100644
--- a/server/controllers/auth.js
+++ b/server/controllers/auth.js
@@ -18,6 +18,13 @@ router.get('/logout', function (req, res) {
res.redirect('/')
})
+/**
+ * Register form
+ */
+router.get('/register', function (req, res, next) {
+ res.render('register')
+})
+
/**
* JWT Public Endpoints
*/
diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js
index 8ab56c31..d0af41ef 100644
--- a/server/graph/resolvers/authentication.js
+++ b/server/graph/resolvers/authentication.js
@@ -38,7 +38,7 @@ module.exports = {
AuthenticationMutation: {
async login(obj, args, context) {
try {
- let authResult = await WIKI.models.users.login(args, context)
+ const authResult = await WIKI.models.users.login(args, context)
return {
...authResult,
responseResult: graphHelper.generateSuccess('Login success')
@@ -49,7 +49,7 @@ module.exports = {
},
async loginTFA(obj, args, context) {
try {
- let authResult = await WIKI.models.users.loginTFA(args, context)
+ const authResult = await WIKI.models.users.loginTFA(args, context)
return {
...authResult,
responseResult: graphHelper.generateSuccess('TFA success')
@@ -58,6 +58,22 @@ module.exports = {
return graphHelper.generateError(err)
}
},
+ async register(obj, args, context) {
+ try {
+ await WIKI.models.users.register(args, context)
+ const authResult = await WIKI.models.users.login({
+ username: args.email,
+ password: args.password,
+ strategy: 'local'
+ }, context)
+ return {
+ jwt: authResult.jwt,
+ responseResult: graphHelper.generateSuccess('Registration success')
+ }
+ } catch (err) {
+ return graphHelper.generateError(err)
+ }
+ },
async updateStrategies(obj, args, context) {
try {
for (let str of args.strategies) {
diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql
index aabb4295..e361762a 100644
--- a/server/graph/schemas/authentication.graphql
+++ b/server/graph/schemas/authentication.graphql
@@ -36,6 +36,12 @@ type AuthenticationMutation {
securityCode: String!
): DefaultResponse
+ register(
+ email: String!
+ password: String!
+ name: String!
+ ): AuthenticationRegisterResponse
+
updateStrategies(
strategies: [AuthenticationStrategyInput]
): DefaultResponse @auth(requires: ["manage:system"])
@@ -69,6 +75,11 @@ type AuthenticationLoginResponse {
tfaLoginToken: String
}
+type AuthenticationRegisterResponse {
+ responseResult: ResponseStatus
+ jwt: String
+}
+
input AuthenticationStrategyInput {
isEnabled: Boolean!
key: String!
diff --git a/server/helpers/error.js b/server/helpers/error.js
index 67e13d4f..3643372b 100644
--- a/server/helpers/error.js
+++ b/server/helpers/error.js
@@ -1,30 +1,44 @@
-class BaseError extends Error {
- constructor (message) {
- super(message)
- this.name = this.constructor.name
- Error.captureStackTrace(this, this.constructor)
- }
-}
-
-class AuthGenericError extends BaseError { constructor (message = 'An unexpected error occured during login.') { super(message) } }
-class AuthLoginFailed extends BaseError { constructor (message = 'Invalid email / username or password.') { super(message) } }
-class AuthProviderInvalid extends BaseError { constructor (message = 'Invalid authentication provider.') { super(message) } }
-class AuthTFAFailed extends BaseError { constructor (message = 'Incorrect TFA Security Code.') { super(message) } }
-class AuthTFAInvalid extends BaseError { constructor (message = 'Invalid TFA Security Code or Login Token.') { super(message) } }
-class BruteInstanceIsInvalid extends BaseError { constructor (message = 'Invalid Brute Force Instance.') { super(message) } }
-class BruteTooManyAttempts extends BaseError { constructor (message = 'Too many attempts! Try again later.') { super(message) } }
-class LocaleInvalidNamespace extends BaseError { constructor (message = 'Invalid locale or namespace.') { super(message) } }
-class UserCreationFailed extends BaseError { constructor (message = 'An unexpected error occured during user creation.') { super(message) } }
+const CustomError = require('custom-error-instance')
module.exports = {
- BaseError,
- AuthGenericError,
- AuthLoginFailed,
- AuthProviderInvalid,
- AuthTFAFailed,
- AuthTFAInvalid,
- BruteInstanceIsInvalid,
- BruteTooManyAttempts,
- LocaleInvalidNamespace,
- UserCreationFailed
+ AuthGenericError: CustomError('AuthGenericError', {
+ message: 'An unexpected error occured during login.',
+ code: 1001
+ }),
+ AuthLoginFailed: CustomError('AuthLoginFailed', {
+ message: 'Invalid email / username or password.',
+ code: 1002
+ }),
+ AuthProviderInvalid: CustomError('AuthProviderInvalid', {
+ message: 'Invalid authentication provider.',
+ code: 1003
+ }),
+ AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', {
+ message: 'An account already exists using this email address.',
+ code: 1004
+ }),
+ AuthTFAFailed: CustomError('AuthTFAFailed', {
+ message: 'Incorrect TFA Security Code.',
+ code: 1005
+ }),
+ AuthTFAInvalid: CustomError('AuthTFAInvalid', {
+ message: 'Invalid TFA Security Code or Login Token.',
+ code: 1006
+ }),
+ BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', {
+ message: 'Invalid Brute Force Instance.',
+ code: 1007
+ }),
+ BruteTooManyAttempts: CustomError('BruteTooManyAttempts', {
+ message: 'Too many attempts! Try again later.',
+ code: 1008
+ }),
+ LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {
+ message: 'Invalid locale or namespace.',
+ code: 1009
+ }),
+ UserCreationFailed: CustomError('UserCreationFailed', {
+ message: 'An unexpected error occured during user creation.',
+ code: 1010
+ })
}
diff --git a/server/models/users.js b/server/models/users.js
index e5c44023..28e24c1e 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -292,4 +292,23 @@ module.exports = class User extends Model {
}
throw new WIKI.Error.AuthTFAInvalid()
}
+
+ static async register ({ email, password, name }, context) {
+ const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
+ if (!usr) {
+ await WIKI.models.users.query().insert({
+ provider: 'local',
+ email,
+ name,
+ password,
+ locale: 'en',
+ defaultEditor: 'markdown',
+ tfaIsActive: false,
+ isSystem: false
+ })
+ return true
+ } else {
+ throw new WIKI.Error.AuthAccountAlreadyExists()
+ }
+ }
}
diff --git a/server/views/register.pug b/server/views/register.pug
new file mode 100644
index 00000000..952885f8
--- /dev/null
+++ b/server/views/register.pug
@@ -0,0 +1,5 @@
+extends master.pug
+
+block body
+ #root.is-fullscreen
+ register