You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CoreID/app/assets/app/dash/profile/EditProfile.component.js

501 lines
20 KiB

import { Component } from '../../../lib/vues6/vues6.js'
import { session } from '../../service/Session.service.js'
import { password_service } from '../../service/Password.service.js'
import { auth_api } from '../../service/AuthApi.service.js'
import { location_service } from '../../service/Location.service.js'
import { message_service } from '../../service/Message.service.js'
import { utility } from '../../service/Utility.service.js'
import { profile_service } from '../../service/Profile.service.js'
const template = `
<div class="coreid-profile-container mb-5">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-8 offset-2 col-sm-4 offset-sm-0">
<img src="/api/v1/profile/me/photo" :alt="t['profile.profile_photo']" ref="photo" class="img-fluid">
<div class="overlay">
<button class="btn btn-outline-light" @click="on_profile_change_click">{{ t['common.change'] }}</button>
</div>
</div>
<div class="col-sm-8 offset-sm-0 col-12 text-sm-left text-center pad-top">
<div class="card-title"><h3>{{ profile_first }} {{ profile_last }}</h3></div>
<div class="card-subtitle mb-2 text-muted">{{ profile_tagline }}</div>
</div>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<h4>{{ t['profile.basic_profile'] }}</h4>
<div class="row">
<div class="col-12 col-md-6 form-group">
<label for="coreid-profile-first-input">{{ t['register.first_name'] }}</label>
<input
type="text"
class="form-control"
id="coreid-profile-first-input"
:placeholder="t['profile.placeholder_first']"
v-model="profile_first"
@keyup="on_key_up($event)"
>
</div>
<div class="col-12 col-md-6 form-group">
<label for="coreid-profile-last-input">{{ t['register.last_name'] }}</label>
<input
type="text"
class="form-control"
id="coreid-profile-last-input"
:placeholder="t['profile.placeholder_last']"
v-model="profile_last"
@keyup="on_key_up($event)"
>
</div>
</div>
<div class="row">
<div class="col-12 form-group">
<label for="coreid-profile-email-input">{{ t['register.email'] }}</label>
<input
type="email"
class="form-control"
id="coreid-profile-email-input"
:placeholder="t['profile.placeholder_email'] "
v-model="profile_email"
ref="email_input"
@keyup="on_key_up($event)"
>
</div>
</div>
<div class="row">
<div class="col-12 form-group">
<label for="coreid-profile-tag-input">{{ t['profile.tagline'] }}</label>
<input
type="text"
class="form-control"
id="coreid-profile-tag-input"
v-model="profile_tagline"
@keyup="on_key_up($event)"
>
</div>
</div>
</li>
<li class="list-group-item text-right font-italic text-muted">
{{ form_message }}
</li>
<li class="list-group-item" v-if="!user_id || user_id === 'me'">
<h4>{{ t['password.password'] }}</h4>
<p class="font-italic" v-if="last_reset">{{ t['profile.pw_last_reset'].replace('LAST_RESET', last_reset) }}</p>
<button
type="button"
class="btn btn-primary btn-sm"
@click="change_password"
>{{ t['password.change'] }}</button>
</li>
<li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')">
<h4>{{ t['mfa.mfa'] }}</h4>
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
<p>{{ t['profile.mfa_2'].replace(/APP_NAME/g, app_name) }}</p>
<button class="btn btn-success btn-sm" type="button" @click="enable_mfa">{{ t['mfa.enable'] }}</button>
</li>
<li class="list-group-item" v-if="has_mfa && (!user_id || user_id === 'me')">
<h4>{{ t['mfa.mfa'] }}</h4>
<p class="font-italic">{{ t['profile.mfa_enabled_on'].replace('MFA_ENABLED', mfa_enable_date) }}</p>
<button
class="btn btn-danger btn-sm"
type="button"
@click="disable_mfa"
>{{ t['mfa.disable'] }}</button>
<h6 class="pad-top">{{ t['profile.app_pws'] }}</h6>
<p>{{ t['profile.app_pw_1'].replace('APP_NAME', app_name) }}</p>
<p>{{ t['profile.app_pw_2'] }}</p>
<p>{{ t['profile.app_pw_3'] }}</p>
<p class="font-italic text-muted" v-if="app_passwords.length > 0">{{ t['profile.app_pw_remaining.'+(app_passwords.length === 1 ? 'one' : 'many')].replace('NUM_PWS', app_passwords.length) }}</p>
<ul class="list-group mb-4" v-if="app_passwords.length > 0">
<li class="list-group-item" v-for="pw of app_passwords">
<div class="row">
<div class="col-9">
{{ pw.name }}
<br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span>
</div>
<div class="col-3 my-auto">
<button
class="btn btn-sm btn-danger"
type="button"
@click="deactivate_app_password($event, pw)"
>{{ t['common.deactivate'] }}</button>
</div>
</div>
</li>
</ul>
<button class="btn btn-sm btn-primary" @click="on_click_generate_app_password">{{ t['profile.gen_new'] }}</button>
<h6 class="pad-top">{{ t['profile.recovery_codes'] }}</h6>
<p>{{ t['profile.recovery_1'] }}</p>
<span v-if="!has_mfa_recovery">
<p class="font-italic">{{ t['profile.no_recovery'] }}</p>
<button class="btn btn-sm btn-success" @click="on_mfa_recovery_generate">{{ t['profile.generate_recovery'] }}</button>
</span>
<span v-if="has_mfa_recovery">
<p class="font-italic">{{ t['profile.recovery_gen_on'].replace('MFA_RECOVERY', mfa_recovery_date) }} {{ t['profile.codes_remaining.'+(mfa_recovery_codes === 1 ? 'one' : 'many')].replace('NUM_CODES', mfa_recovery_codes) }}</p>
<button class="btn btn-sm btn-success" @click="on_mfa_recovery_generate">{{ t['profile.regenerate_recovery'] }}</button>
</span>
</li>
<li class="list-group-item" v-if="notify_loaded">
<h4>{{ t['profile.notifications'] }}</h4>
<p v-html="t['profile.notify_explainer_1'].replace(/APP_NAME/g, app_name)"></p>
<p>{{ t['profile.notify_explainer_2'].replace(/APP_NAME/g, app_name) }}</p>
<div class="row">
<div class="col-12 form-group">
<input
type="text"
class="form-control"
:placeholder="t['profile.example_gateway_url']"
id="coreid-profile-notify-gateway-url"
v-model="notify_gateway_url"
>
</div>
<div class="col-12 form-group">
<input
type="text"
class="form-control"
:placeholder="t['profile.app_key']"
id="coreid-profile-notify-app-key"
v-model="notify_app_key"
>
</div>
<div class="col-12 mt-2">
<button class="btn btn-sm btn-primary" @click="on_notifications_save">{{ t['profile.save_notify'] }}</button>
<button
class="btn btn-sm btn-secondary"
v-if="notify_app_key && notify_gateway_url && notify_enabled"
@click="send_test_notification"
>{{ t['profile.test_notify'] }}</button>
</div>
</div>
</li>
</ul>
</div>
<coreid-form-app-password
v-if="!user_id || user_id === 'me'"
ref="app_password_modal"
@modal-success="load_app_passwords"
></coreid-form-app-password>
<coreid-profile-photo-uploader
ref="profile_photo_uploader"
:user-id="user_id"
@upload="on_profile_photo_upload"
></coreid-profile-photo-uploader>
</div>
`
export default class EditProfileComponent extends Component {
static get selector() { return 'coreid-profile-edit' }
static get template() { return template }
static get props() { return ['user_id'] }
profile_first = ''
profile_last = ''
profile_email = ''
profile_tagline = ''
last_reset = ''
mfa_enable_date = ''
has_mfa_recovery = false
mfa_recovery_date = ''
mfa_recovery_codes = 0
form_message = 'No changes.'
has_mfa = false
ready = false
notify_gateway_url = ''
notify_app_key = ''
notify_enabled = false
notify_created_on = ''
notify_loaded = false
app_passwords = []
app_name = ''
t = {}
on_key_up = ($event) => {}
async vue_on_create() {
this.t = await T(
'profile.profile_photo',
'common.change',
'profile.basic_profile',
'register.first_name',
'profile.placeholder_first',
'register.last_name',
'profile.placeholder_last',
'register.email',
'profile.placeholder_email',
'profile.tagline',
'password.password',
'password.change',
'profile.pw_last_reset',
'mfa.mfa',
'profile.mfa_enabled_on',
'mfa.disable',
'profile.app_pws',
'profile.issued',
'common.deactivate',
'profile.app_pw_remaining.one',
'profile.app_pw_remaining.many',
'profile.gen_new',
'profile.recovery_codes',
'profile.recovery_1',
'profile.no_recovery',
'profile.generate_recovery',
'profile.recovery_gen_on',
'profile.codes_remaining.one',
'profile.codes_remaining.many',
'profile.regenerate_recovery',
'profile.app_pw_1',
'profile.app_pw_2',
'profile.app_pw_3',
'profile.mfa_1',
'profile.mfa_2',
'mfa.enable',
'profile.notifications',
'profile.notify_explainer_1',
'profile.notify_explainer_2',
'profile.app_key',
'profile.example_gateway_url',
'profile.save_notify',
'profile.test_notify'
)
this.app_name = session.get('app.name')
this.load().then(() => {
this.ready = true
})
const save = utility.debounce(this.save_form.bind(this))
this.on_key_up = () => {
this.form_message = 'Saving...'
save()
}
}
get_submit_data() {
return {
first_name: this.profile_first,
last_name: this.profile_last,
email: this.profile_email,
tagline: this.profile_tagline,
user_id: this.user_id || 'me',
}
}
on_profile_change_click() {
this.$refs.profile_photo_uploader.show()
}
async on_profile_photo_upload() {
this.$refs.profile_photo_uploader.close()
let src = this.$refs.photo.src
if ( src.indexOf('?') > 0 ) src = src.split('?')[0]
this.$refs.photo.src = `${src}?i=${(new Date).getTime()}`
}
valid_email() {
return this.$refs.email_input.validity.valid
}
async save_form($event) {
const submit_data = this.get_submit_data()
try {
if ( !this.valid_email() ) {
this.form_message = 'Invalid e-mail address.'
} else {
await profile_service.update_profile(submit_data)
this.form_message = 'All changes saved.'
}
} catch(e) {
this.form_message = 'Unknown error occurred while saving.'
}
}
populate_from_session() {
this.profile_first = session.get('user.first_name')
this.profile_last = session.get('user.last_name')
this.profile_email = session.get('user.email')
this.profile_tagline = session.get('user.tagline')
}
async load() {
const result = await profile_service.get_profile(this.user_id || 'me')
if ( !result ) throw new Error('Unable to load profile!')
this.profile_first = result.first_name
this.profile_last = result.last_name
this.profile_email = result.email
this.profile_tagline = result.tagline
const notify_config = await profile_service.get_notify(this.user_id || 'me')
if ( !notify_config || !notify_config.has_config ) {
this.notify_app_key = ''
this.notify_enabled = false
this.notify_created_on = ''
this.notify_gateway_url = ''
} else if ( notify_config && notify_config.has_config ) {
this.notify_app_key = notify_config.config.application_key
this.notify_enabled = notify_config.config.active
this.notify_created_on = notify_config.config.created_on
this.notify_gateway_url = notify_config.config.gateway_url
}
this.notify_loaded = true
if ( !this.user_id || this.user_id === 'me' ) {
const reset = (await password_service.get_resets()).reverse()[0]
if (reset && reset.reset_on) {
this.last_reset = (new Date(reset.reset_on)).toLocaleDateString()
}
const mfa = await auth_api.has_mfa()
this.has_mfa = mfa && mfa.mfa_enabled
if (this.has_mfa) {
this.mfa_enable_date = (new Date(mfa.mfa_enable_date)).toLocaleDateString()
const result = await auth_api.has_mfa_recovery()
if ( result && result.has_recovery ) {
this.has_mfa_recovery = true
this.mfa_recovery_date = (new Date(result.generated)).toLocaleDateString()
this.mfa_recovery_codes = result.remaining_codes
}
}
await this.load_app_passwords()
}
}
async load_app_passwords() {
let app_pws = await auth_api.app_passwords()
if ( !Array.isArray(app_pws) ) app_pws = []
this.app_passwords = app_pws.map(x => {
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
return x
})
}
disable_mfa() {
location_service.redirect('/auth/mfa/disable')
}
enable_mfa() {
location_service.redirect('/auth/mfa/setup')
}
change_password() {
location_service.redirect('/password/reset')
}
on_click_generate_app_password() {
this.$refs.app_password_modal.trigger()
}
async deactivate_app_password($event, pw) {
message_service.modal({
title: 'Deactivate app password?',
message: `You are about to deactivate the app password for ${pw.name}. If you do this, ${pw.name} will no longer be able to sign-in on your behalf. Continue?`,
buttons: [
{
text: 'Cancel',
type: 'close',
},
{
text: 'Deactivate',
type: 'close',
class: ['btn', 'btn-danger'],
on_click: async () => {
await auth_api.delete_app_password(pw.uuid)
await this.load_app_passwords()
},
},
],
})
}
async on_mfa_recovery_generate($event) {
if ( !this.has_mfa ) return
if ( !this.has_mfa_recovery ) {
await this.generate_mfa_recovery()
} else {
message_service.modal({
title: 'Are you sure?',
message: 'There are already MFA recovery codes associated with your account. If you re-generate them, you will be unable to use the old ones. Continue?',
buttons: [
{
text: 'Cancel',
type: 'close',
},
{
text: 'Re-generate',
type: 'close',
class: ['btn', 'btn-warning'],
on_click: async () => {
await this.generate_mfa_recovery()
},
},
],
})
}
}
async generate_mfa_recovery() {
const codes = await auth_api.generate_mfa_recovery()
if ( codes ) {
this.display_mfa_recovery_modal(codes)
} else {
message_service.alert({
type: 'error',
message: 'An unknown error occurred while attempting to generate MFA recovery codes.'
})
}
await this.load()
}
display_mfa_recovery_modal(codes) {
const code_display = codes.map(x => `<li><code>${x}</code></li>`).join('\n')
message_service.modal({
title: 'MFA Recovery Codes',
message: `We've generated recovery codes for your account. You can use these to recover access to your account in the event that you lose your MFA device.
<br><br>
Be sure to put these somewhere safe. After you close this modal, they will disappear:
<br><br>
<ul>
${code_display}
</ul>`,
buttons: [
{
text: 'Close',
type: 'close',
},
],
})
}
async on_notifications_save() {
const data = {
user_id: this.user_id || 'me',
app_key: this.notify_app_key,
gateway_url: this.notify_gateway_url,
}
await profile_service.update_notify(data)
await this.load()
}
async send_test_notification() {
await profile_service.test_notify({ user_id: this.user_id || 'me' })
}
}