Support authentication flows for login/register
This commit is contained in:
parent
d86ee3b1e3
commit
0d3781f3aa
11 changed files with 481 additions and 139 deletions
293
src/components/InteractiveAuth.vue
Normal file
293
src/components/InteractiveAuth.vue
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
<template>
|
||||
<v-fade-transition>
|
||||
<v-container fluid class="mt-40" v-if="step == steps.CAPTCHA">
|
||||
<div class="ma-3" flat ref="captcha" id="captcha">
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.ENTER_EMAIL">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.email") }}</div>
|
||||
<v-text-field v-model="email" color="black" :rules="emailRules" type="email" maxlength="200"
|
||||
background-color="white" v-on:keyup.enter="onEmailEntered(email)" autofocus solo></v-text-field>
|
||||
<v-btn :disabled="!emailIsValid" color="black" depressed class="filled-button"
|
||||
@click.stop="onEmailEntered(email)">
|
||||
{{ $t("login.send_verification") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.TERMS">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div class="text-left font-weight-light">{{ $t("login.terms") }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div v-for="(policy) in this.policies" :key="policy.id">
|
||||
<v-checkbox class="mt-0" v-model="policy.accepted">
|
||||
<template v-slot:label>
|
||||
<a target="_blank" :href="policy.url" @click.stop>{{ policy.name }}
|
||||
</a>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<v-btn color="black" :disabled="!this.allPoliciesAccepted" depressed class="filled-button"
|
||||
@click.stop="onPoliciesAccepted()">
|
||||
{{ $t("login.accept_terms") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-container fluid class="mt-40" v-if="step == steps.AWAITING_EMAIL_VERIFICATION">
|
||||
<v-row cols="12" align="center" justify="center">
|
||||
<v-col sm="8" align="center">
|
||||
<div>
|
||||
<div class="text-left font-weight-light">{{ $t("login.sent_verification", { email: this.email }) }}</div>
|
||||
<v-progress-circular style="display: inline-flex" indeterminate color="primary"
|
||||
size="20"></v-progress-circular>
|
||||
</div>
|
||||
<v-btn color="black" depressed class="filled-button" @click.stop="onEmailResend()">
|
||||
{{ $t("login.resend_verification") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import util from "../plugins/utils";
|
||||
|
||||
const steps = Object.freeze({
|
||||
INITIAL: 0,
|
||||
CAPTCHA: 1,
|
||||
TERMS: 2,
|
||||
EXTERNAL_AUTH: 3,
|
||||
ENTER_EMAIL: 4,
|
||||
AWAITING_EMAIL_VERIFICATION: 5,
|
||||
});
|
||||
|
||||
export default {
|
||||
name: "InteractiveAuth",
|
||||
data() {
|
||||
return {
|
||||
steps,
|
||||
step: steps.INITIAL,
|
||||
emailRules: [
|
||||
v => this.validEmail(v) || this.$t("login.email_not_valid")
|
||||
],
|
||||
policies: null,
|
||||
onPoliciesAccepted: () => { },
|
||||
onEmailResend: () => { },
|
||||
onEmailVerify: () => { },
|
||||
email: "",
|
||||
emailVerification: "",
|
||||
emailVerificationSecret: util.randomUser("tokn"),
|
||||
emailVerificationAttempt: 1,
|
||||
emailVerificationSid: null,
|
||||
emailVerificationPollTimer: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
allPoliciesAccepted() {
|
||||
return Object.keys(this.policies).every(id => this.policies[id].accepted);
|
||||
},
|
||||
emailIsValid() {
|
||||
return this.validEmail(this.email);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
validEmail(email) {
|
||||
if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/.test(email)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
registrationFlowHandler(client, authData) {
|
||||
const flows = authData.flows;
|
||||
if (!flows || flows.length == 0) {
|
||||
return Promise.reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
const currentFlow = flows[0];
|
||||
if (!currentFlow || !currentFlow.stages || currentFlow.stages.length == 0) {
|
||||
return Promise.reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
|
||||
// TODO - For now hardcoded to flows[0]
|
||||
const completedStages = authData.completed || [];
|
||||
const remainingStages = currentFlow.stages.filter((stage) => {
|
||||
return !completedStages.includes(stage);
|
||||
});
|
||||
|
||||
const nextStage = remainingStages[0];
|
||||
|
||||
const submitStageData = (resolve, reject, data) => {
|
||||
this.step = steps.CREATING;
|
||||
resolve(client.registerRequest({ auth: data }).catch(err => {
|
||||
if (err.httpStatus == 401 && err.data) {
|
||||
// Start next stage!
|
||||
return this.registrationFlowHandler(client, err.data);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
switch (nextStage) {
|
||||
case "m.login.recaptcha":
|
||||
this.step = steps.CAPTCHA;
|
||||
return new Promise((resolve, reject) => {
|
||||
let script = document.createElement("script");
|
||||
script.src = "https://www.google.com/recaptcha/api.js?onload=onReCaptchaLoaded&render=explicit";
|
||||
window.onReCaptchaLoaded = (ignoredev) => {
|
||||
const params = authData.params[nextStage];
|
||||
if (!params || !params.public_key) {
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
const site_key = params.public_key;
|
||||
window.grecaptcha.render('captcha', {
|
||||
'sitekey': site_key,
|
||||
'theme': 'light',
|
||||
callback: (captcha_response) => {
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
response: captcha_response
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
document.body.append(script);
|
||||
});
|
||||
case "m.login.terms":
|
||||
this.step = steps.TERMS;
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = authData.params[nextStage];
|
||||
if (!params || !params.policies) {
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
|
||||
let policies = [];
|
||||
Object.keys(params.policies).forEach((policyId) => {
|
||||
const policy = params.policies[policyId];
|
||||
let localized = policy[this.$i18n.locale] || policy["en"] || policy[Object.keys(policy).filter(k => k != "version")[0]];
|
||||
if (!localized) {
|
||||
// Did not find info for this policy!
|
||||
return reject(this.$t('login.no_supported_flow'));
|
||||
}
|
||||
policies.push({
|
||||
id: policyId,
|
||||
name: localized.name,
|
||||
url: localized.url,
|
||||
accepted: false
|
||||
});
|
||||
});
|
||||
this.onPoliciesAccepted = () => {
|
||||
// Button should not be enabled if not all are accepted, but double check here...
|
||||
if (this.allPoliciesAccepted) {
|
||||
const data = {
|
||||
session: authData.session,
|
||||
type: nextStage,
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
}
|
||||
}
|
||||
this.policies = policies;
|
||||
});
|
||||
|
||||
case "m.login.email.identity":
|
||||
this.step = steps.ENTER_EMAIL;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.onEmailEntered = (email) => {
|
||||
this.step = steps.CREATING;
|
||||
this.emailVerificationAttempt = 1;
|
||||
client.requestRegisterEmailToken(email, this.emailVerificationSecret, this.emailVerificationAttempt, null)
|
||||
.then(response => {
|
||||
this.emailVerificationSid = response.sid;
|
||||
this.step = steps.AWAITING_EMAIL_VERIFICATION;
|
||||
this.onEmailResend = () => {
|
||||
this.emailVerificationAttempt += 1;
|
||||
client.requestRegisterEmailToken(email, this.emailVerificationSecret, this.emailVerificationAttempt, null)
|
||||
.then(response => {
|
||||
this.emailVerificationSid = response.sid; // Update the SID (TODO: will it really change?)
|
||||
})
|
||||
.catch(ignorederr => { });
|
||||
},
|
||||
this.onEmailVerify = async () => {
|
||||
const data = {
|
||||
type: nextStage,
|
||||
threepid_creds: {
|
||||
sid: this.emailVerificationSid,
|
||||
client_secret: this.emailVerificationSecret
|
||||
},
|
||||
session: authData.session,
|
||||
};
|
||||
console.log("Polling...");
|
||||
await client.registerRequest({ auth: data })
|
||||
.then((result) => {
|
||||
// Yes, email is verified!
|
||||
clearTimeout(this.emailVerificationPollTimer);
|
||||
this.emailVerificationPollTimer = null;
|
||||
resolve(result);
|
||||
})
|
||||
.catch(ignorederr => {
|
||||
// Continue polling
|
||||
});
|
||||
};
|
||||
this.emailVerificationPollTimer = setInterval(this.onEmailVerify, 5000);
|
||||
})
|
||||
.catch((ignorederr) => {
|
||||
console.error("ERROR", ignorederr);
|
||||
return reject(this.$t('login.no_supported_flow'))
|
||||
});
|
||||
}
|
||||
});
|
||||
default:
|
||||
this.step = steps.EXTERNAL_AUTH;
|
||||
return new Promise((resolve, reject) => {
|
||||
let fallbackUrl = client.getFallbackAuthUrl(nextStage, authData.session);
|
||||
var popupWindow;
|
||||
|
||||
var eventListener = function (ev) {
|
||||
// check it's the right message from the right place.
|
||||
if (ev.data !== "authDone" || !fallbackUrl.startsWith(ev.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// close the popup
|
||||
popupWindow.close();
|
||||
window.removeEventListener("message", eventListener);
|
||||
|
||||
const data = {
|
||||
session: authData.session,
|
||||
};
|
||||
submitStageData(resolve, reject, data);
|
||||
};
|
||||
|
||||
window.addEventListener("message", eventListener);
|
||||
popupWindow = window.open(fallbackUrl, "_blank", "popup=true");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue