Start client-side locale support

This commit is contained in:
garrettmills
2020-05-31 18:00:05 -05:00
parent d2ae9c43e8
commit c956628c53
16 changed files with 324 additions and 66 deletions

View File

@@ -7,13 +7,13 @@ const template = `
<div class="coreid-auth-page-inner">
<div class="coreid-header font-weight-light">{{ app_name }}</div>
<div class="coreid-message">
Your account has multi-factor authentication enabled. Please enter the code generated by your authenticator app to continue.
{{ t['mfa.challenge_prompt'] }}
</div>
<div class="form-group">
<input
class="form-control"
type="number"
placeholder="2FA Code"
:placeholder="t['mfa.mfa_code']"
v-model="verify_code"
@keyup="on_key_up"
name="verify_code"
@@ -28,7 +28,7 @@ const template = `
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
<small
class="mr-3"
><a href="#" class="text-secondary" @click="on_do_recovery">Lost your MFA device?</a></small>
><a href="#" class="text-secondary" @click="on_do_recovery">{{ t['mfa.lost_device'] }}</a></small>
</div>
</div>
`
@@ -45,6 +45,19 @@ export default class MFAChallengePage extends Component {
error_message = ''
other_message = ''
t = {}
async vue_on_create() {
this.t = await T(
'mfa.challenge_prompt',
'mfa.mfa_code',
'mfa.lost_device',
'mfa.invalid_code',
'mfa.success'
)
console.log(this)
}
async watch_verify_code(new_verify_code, old_verify_code) {
if ( new_verify_code.length === 6 ) {
@@ -53,11 +66,11 @@ export default class MFAChallengePage extends Component {
this.verify_success = result && result.is_valid
if ( !this.verify_success ) {
this.other_message = ''
this.error_message = `Uh, oh! It looks like that's not the right code. Please try again.`
this.error_message = this.t['mfa.invalid_code']
this.loading = false
} else {
this.error_message = ''
this.other_message = `Success! Redirecting...`
this.other_message = this.t['mfa.success']
await location_service.redirect(result.next_destination, 1500)
}
} else if ( new_verify_code.length > 6 ) {

View File

@@ -9,17 +9,13 @@ const template = `
<div class="coreid-header font-weight-light">{{ app_name }}</div>
<span v-if="step === 0">
<div class="coreid-message">
This process will disable multi-factor authentication on your account.
<br><br>
For security reasons, this will sign you out of all devices. It will also deactivate any existing app passwords you have generated.
<br><br>
Are you sure you want to continue?
<span v-html="t['mfa.disable_prompt']"></span>
</div>
<div v-if="error_message" class="error-message">{{ error_message }}</div>
<div v-if="other_message" class="other-message">{{ other_message }}</div>
<div class="buttons text-right pad-top">
<button type="button" class="btn btn-primary" @click="back_click">Cancel</button>
<button type="button" class="btn btn-primary" @click="continue_click">Disable MFA</button>
<button type="button" class="btn btn-primary" @click="back_click">{{ t['common.cancel'] }}</button>
<button type="button" class="btn btn-primary" @click="continue_click">{{ t['mfa.disable'] }}</button>
</div>
</span>
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
@@ -37,9 +33,18 @@ export default class MFADisableComponent extends Component {
loading = false
error_message = ''
other_message = ''
t = {}
vue_on_create() {
async vue_on_create() {
this.app_name = session.get('app.name')
this.t = await T(
'common.cancel',
'common.unknown_error',
'mfa.disable_success',
'mfa.disable',
'mfa.disable_prompt'
)
}
async back_click() {
@@ -51,10 +56,10 @@ export default class MFADisableComponent extends Component {
this.loading = true
const success = await auth_api.mfa_disable()
if ( success ) {
this.other_message = 'MFA was successfully disabled. You\'ll now sign-in normally.'
this.other_message = this.t['mfa.disable_success']
await location_service.redirect('/dash/profile', 3000)
} else {
this.error_message = 'An unknown error occurred while trying to disable MFA. Let\'s try again...'
this.error_message = this.t['common.unknown_error']
await location_service.reload(4000)
}
}

View File

@@ -7,13 +7,13 @@ const template = `
<div class="coreid-auth-page-inner">
<div class="coreid-header font-weight-light">{{ app_name }}</div>
<div class="coreid-message">
To recover access to your account, you can enter one of the generated MFA recovery codes:
{{ t['mfa.recover_prompt'] }}
</div>
<div class="form-group">
<input
class="form-control"
type="text"
placeholder="Recovery Code"
:placeholder="t['mfa.recovery_code']"
v-model="recovery_code"
@keyup="on_key_up"
name="recovery_code"
@@ -27,7 +27,7 @@ const template = `
<div v-if="other_message" class="other-message">{{ other_message }}</div>
<small
class="mr-3" v-if="!loading"
><a href="#" class="text-secondary" @click="on_do_challenge">Have a normal MFA code?</a></small>
><a href="#" class="text-secondary" @click="on_do_challenge">{{ t['mfa.normal_code'] }}</a></small>
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
</div>
</div>
@@ -43,8 +43,17 @@ export default class MFARecoveryComponent extends Component {
recovery_code = ''
error_message = ''
other_message = ''
t = {}
async vue_on_create() {
this.t = await T(
'mfa.recover_prompt',
'mfa.recovery_code',
'mfa.normal_code',
'mfa.recover_success',
'mfa.invalid_code'
)
this.$nextTick(() => {
this.$refs.verify_input.focus()
})
@@ -57,11 +66,11 @@ export default class MFARecoveryComponent extends Component {
this.loading = true
const result = await auth_api.attempt_mfa_recovery(this.recovery_code)
if ( result && result.success ) {
this.other_message = `Success! There are only ${result.remaining_codes} recovery codes remaining. Let's get you on your way...`
this.other_message = this.t['mfa.recover_success'].replace('NUM_CODES', result.remaining_codes)
await location_service.redirect(result.next_destination, 5000)
} else {
this.loading = false
this.error_message = 'Hm. It doesn\'t look like that code is valid.'
this.error_message = this.t['mfa.invalid_code']
}
}
}

View File

@@ -16,7 +16,7 @@ const template = `
id="coreid-login-form-username"
name="username"
class="form-control"
placeholder="Username"
:placeholder="t['login.username']"
v-model="username"
autofocus
@keyup="on_key_up"
@@ -29,7 +29,7 @@ const template = `
id="coreid-login-form-password"
name="password"
class="form-control"
placeholder="Password"
:placeholder="t['login.password']"
v-model="password"
:disabled="loading"
@keyup="on_key_up"
@@ -42,12 +42,12 @@ const template = `
<small
class="mr-3"
v-if="!step_two && !loading && registration_enabled"
><a href="#" class="text-secondary" @click="on_register_click">Need an account?</a></small>
><a href="#" class="text-secondary" @click="on_register_click">{{ t['login.need_account'] }}</a></small>
<small
class="mr-3"
v-if="!auth_user && !loading"
><a href="#" class="text-secondary" @click="on_forgot_password">Forgot password?</a></small>
<button type="button" class="btn btn-primary" :disabled="loading" v-if="step_two" v-on:click="back_click">Back</button>
><a href="#" class="text-secondary" @click="on_forgot_password">{{ t['login.forgot_password'] }}</a></small>
<button type="button" class="btn btn-primary" :disabled="loading" v-if="step_two" v-on:click="back_click">{{ t['common.back'] }}</button>
<button type="button" class="btn btn-primary" :disabled="loading || btn_disabled" v-on:click="step_click">{{ button_text }}</button>
</div>
</form>
@@ -65,7 +65,7 @@ export default class AuthLoginForm extends Component {
username = ''
password = ''
button_text = 'Next'
button_text = ''
step_two = false
btn_disabled = true
loading = false
@@ -74,6 +74,8 @@ export default class AuthLoginForm extends Component {
allow_back = true
auth_user = false
t = {}
watch_username(new_username, old_username) {
this.btn_disabled = !new_username
}
@@ -81,7 +83,7 @@ export default class AuthLoginForm extends Component {
back_click() {
if ( !this.allow_back ) return;
this.step_two = false
this.button_text = 'Next'
this.button_text = this.t['common.next']
}
async vue_on_create() {
@@ -92,6 +94,28 @@ export default class AuthLoginForm extends Component {
this.username = auth_user
await this.step_click()
}
// Batch-load translations all at once to make fewer requests
this.t = await T(
'common.continue',
'common.unknown_error',
'common.cancel',
'common.request',
'common.back',
'common.next',
'login.username',
'login.password',
'login.need_account',
'login.forgot_password',
'login.username_invalid',
'login.reset_password',
'login.reset_password_prompt',
'login.reset_password_success',
'login.success',
'login.get_started'
)
this.button_text = this.t['common.next']
}
async on_key_up(event) {
@@ -110,17 +134,17 @@ export default class AuthLoginForm extends Component {
try {
const is_valid = await auth_api.validate_username(this.username)
if ( !is_valid ) {
this.error_message = 'That username is invalid. Please try again.'
this.error_message = this.t['login.username_invalid']
} else {
this.step_two = true
this.button_text = 'Continue'
this.button_text = this.t['common.continue']
this.error_message = ''
this.$nextTick(() => {
this.$refs.password_input.focus()
})
}
} catch (e) {
this.error_message = 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
this.error_message = this.t['common.unknown_error']
}
this.loading = false
} else {
@@ -139,27 +163,27 @@ export default class AuthLoginForm extends Component {
const result = await auth_api.attempt(attempt_vars)
if ( !result.success ) {
this.error_message = result.message || 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
this.error_message = result.message || this.t['common.unknown_error']
this.loading = false
return
}
this.other_message = 'Success! Let\'s get you on your way...'
this.other_message = this.t['login.success']
await location_service.redirect(result.next, 1500)
}
}
on_register_click() {
this.loading = true
this.other_message = 'Okay! Let\'s get started...'
this.other_message = this.t['login.get_started']
location_service.redirect('/auth/register', 1500) // TODO get this dynamically
}
async on_forgot_password() {
console.log(message_service)
await message_service.modal({
title: 'Reset Password',
message: 'If you have forgotten your password, you can request a reset e-mail to be sent to your account. Enter your e-mail address:',
title: this.t['login.reset_password'],
message: this.t['login.reset_password_prompt'],
inputs: [
{
type: 'email',
@@ -170,17 +194,17 @@ export default class AuthLoginForm extends Component {
buttons: [
{
type: 'close',
text: 'Cancel',
text: this.t['common.cancel'],
},
{
type: 'close',
class: ['btn', 'btn-primary'],
text: 'Request',
text: this.t['common.request'],
on_click: async ($event, { email }) => {
await password_service.request_reset(email)
await message_service.alert({
type: 'success',
message: 'Success! If that e-mail address is associated with a valid ' + this.app_name + ' account, it will receive an e-mail with more instructions shortly.',
message: this.t['login.reset_password_success'].replace('APP_NAME', this.app_name),
})
},
},

View File

@@ -15,7 +15,7 @@ const template = `
id="coreid-registration-form-first"
name="first_name"
class="form-control"
placeholder="First Name"
:placeholder="t['register.first_name']"
v-model="first_name"
autofocus
@keyup="on_key_up"
@@ -29,7 +29,7 @@ const template = `
id="coreid-registration-form-last"
name="last_name"
class="form-control"
placeholder="Last Name"
:placeholder="t['register.last_name']"
v-model="last_name"
autofocus
@keyup="on_key_up"
@@ -43,7 +43,7 @@ const template = `
id="coreid-registration-form-username"
name="username"
class="form-control"
placeholder="Username"
:placeholder="t['login.username']"
v-model="username"
autofocus
@keyup="on_key_up"
@@ -58,7 +58,7 @@ const template = `
id="coreid-registration-form-email"
name="email"
class="form-control"
placeholder="E-Mail"
:placeholder="t['register.email']"
v-model="email"
autofocus
@keyup="on_key_up"
@@ -72,14 +72,14 @@ const template = `
<small
class="mr-3"
v-if="step === 1 && !loading"
><a href="#" class="text-secondary" @click="on_login_click">Already have an account?</a></small>
><a href="#" class="text-secondary" @click="on_login_click">{{ t['register.have_account'] }}</a></small>
<button
type="button"
class="btn btn-primary"
:disabled="loading"
v-if="step > 1"
v-on:click="back_click"
>Back</button>
>{{ t['common.back'] }}</button>
<button
type="button"
class="btn btn-primary"
@@ -104,15 +104,37 @@ export default class RegistrationFormComponent extends Component {
error_message = ''
message = ''
btn_disabled = true
button_text = 'Continue'
button_text = ''
first_name = ''
last_name = ''
username = ''
email = ''
t = {}
async vue_on_create() {
this.message = 'Create an account to continue:'
// Batch-load translated phrases
this.t = await T(
'common.back',
'common.continue',
'common.unknown_error',
'login.username',
'register.first_name',
'register.last_name',
'register.email',
'register.have_account',
'register.create_to_continue',
'register.provide_first_last',
'register.hi_choose_username',
'register.username_in_use',
'register.invalid_email',
'register.email_in_use',
'register.success_password',
'register.login_instead'
)
this.button_text = this.t['common.continue']
this.message = this.t['register.create_to_continue']
}
async step_click() {
@@ -121,18 +143,18 @@ export default class RegistrationFormComponent extends Component {
this.other_message = ''
if ( this.step === 1 ) {
if ( !this.first_name.trim() || !this.last_name.trim() ) {
this.error_message = 'Please provide your first and last name.'
this.error_message = this.t['register.provide_first_last']
this.loading = false
return
}
this.message = `Hi, ${this.first_name.trim()}. Now, you need to choose a username:`
this.message = this.t['register.hi_choose_username'].replace('FIRST_NAME', this.first_name.trim())
this.$nextTick(() => {
this.$refs.input_username.focus()
})
} else if ( this.step === 2 ) {
if ( await auth_api.username_taken(this.username) ) {
this.error_message = 'That username is already taken.'
this.error_message = this.t['register.username_in_use']
this.loading = false
return
}
@@ -144,13 +166,13 @@ export default class RegistrationFormComponent extends Component {
})
} else if ( this.step === 3 ) {
if ( !(await auth_api.validate_email(this.email)) ) {
this.error_message = 'Please provide a valid e-mail address.'
this.error_message = this.t['register.invalid_email']
this.loading = false
return
}
if ( await auth_api.email_taken(this.email) ) {
this.error_message = 'That e-mail address is already taken.'
this.error_message = this.t['register.email_in_use']
this.loading = false
return
}
@@ -163,12 +185,12 @@ export default class RegistrationFormComponent extends Component {
email: this.email,
})
if ( !user ) this.error_message = 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
else this.other_message = 'Welcome! Let\'s get your password set up...'
if ( !user ) this.error_message = this.t['common.unknown_error']
else this.other_message = this.t['register.success_password']
this.btn_disabled = true
return location_service.redirect('/dash', 2000)
} catch (e) {
this.error_message = e.message || 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
this.error_message = e.message || this.t['common.unknown_error']
this.loading = false
return
}
@@ -186,7 +208,7 @@ export default class RegistrationFormComponent extends Component {
this.on_key_up()
if ( this.step === 1 ) {
this.message = 'Create an account to continue:'
this.message = this.t['register.create_to_continue']
}
}
@@ -209,7 +231,7 @@ export default class RegistrationFormComponent extends Component {
on_login_click() {
this.loading = true
this.other_message = 'Okay! We\'ll have you login instead...'
this.other_message = this.t['register.login_instead']
location_service.redirect('/auth/login', 1500)
}

View File

@@ -9,6 +9,9 @@ import RegistrationFormComponent from './auth/register/Form.component.js'
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
import MFARecoveryComponent from './auth/MFARecovery.component.js'
// Create the global
import { T } from './service/Translate.service.js'
const components = {
AuthLoginForm,
AuthPage,

View File

@@ -0,0 +1,57 @@
class TranslateService {
_cache = {}
check_cache(...keys) {
const obj = {}
for ( const key of keys ) {
if ( this._cache[key] )
obj[key] = this._cache[key]
else return false
}
return obj
}
async resolve(key) {
const cache_hit = this.check_cache(key)
if ( cache_hit ) return cache_hit[key]
const result = await axios.get('/api/v1/locale/resolve/'+key)
if ( result && result.data && result.data.data ) {
this._cache[key] = result.data.data
return result.data.data
}
}
async load_module(key) {
const result = await axios.get('/api/v1/locale/module/'+key)
if ( result && result.data && result.data.data ) {
for ( const key in result.data.data ) {
if ( !this._cache[key] ) this._cache[key] = result.data.data[key]
}
return result.data.data
}
}
async batch_resolve(...keys) {
const cache_hit = this.check_cache(...keys)
if ( cache_hit ) return cache_hit
const result = await axios.post('/api/v1/locale/batch', { resolvers: keys })
if ( result && result.data && result.data.data ) {
for ( const key in result.data.data ) {
if ( !this._cache[key] ) this._cache[key] = result.data.data[key]
}
return result.data.data
}
}
}
const translate_service = new TranslateService()
const T = (...keys) => {
if ( keys.length === 1 ) return translate_service.resolve(keys[0])
else return translate_service.batch_resolve(...keys)
}
window.T = T
export { translate_service, T }