353 lines
13 KiB
Vue
353 lines
13 KiB
Vue
<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-start 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 variant="solo"></v-text-field>
|
|
<v-btn :disabled="!emailIsValid" color="black" variant="flat" 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-start 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" variant="flat" 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-start 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" variant="flat" class="filled-button" @click.stop="onEmailResend()">
|
|
{{ $t("login.resend_verification") }}
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
|
|
<v-container fluid class="mt-40" v-if="step == steps.ENTER_TOKEN">
|
|
<v-row cols="12" align="center" justify="center">
|
|
<v-col sm="8" align="center">
|
|
<div class="text-start font-weight-light">{{ $t("login.registration_token") }}</div>
|
|
<v-text-field v-model="token" color="black" :rules="tokenRules" type="text" maxlength="64"
|
|
background-color="white" v-on:keyup.enter="onTokenEntered(token)" autofocus variant="solo"></v-text-field>
|
|
<v-btn :disabled="!tokenIsValid" color="black" variant="flat" class="filled-button"
|
|
@click.stop="onTokenEntered(token)">
|
|
{{ $t("login.send_token") }}
|
|
</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,
|
|
ENTER_TOKEN: 6,
|
|
});
|
|
|
|
export default {
|
|
name: "InteractiveAuth",
|
|
data() {
|
|
return {
|
|
steps,
|
|
step: steps.INITIAL,
|
|
emailRules: [
|
|
v => this.validEmail(v) || this.$t("login.email_not_valid")
|
|
],
|
|
tokenRules: [
|
|
v => this.validToken(v) || this.$t("login.token_not_valid")
|
|
],
|
|
policies: null,
|
|
onPoliciesAccepted: () => { },
|
|
onEmailResend: () => { },
|
|
onEmailVerify: () => { },
|
|
email: "",
|
|
emailVerification: "",
|
|
emailVerificationSecret: util.randomUser("tokn"),
|
|
emailVerificationAttempt: 1,
|
|
emailVerificationSid: null,
|
|
emailVerificationPollTimer: null,
|
|
token: "",
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
allPoliciesAccepted() {
|
|
return Object.keys(this.policies).every(id => this.policies[id].accepted);
|
|
},
|
|
emailIsValid() {
|
|
return this.validEmail(this.email);
|
|
},
|
|
tokenIsValid() {
|
|
return this.validToken(this.token);
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
validEmail(email) {
|
|
if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/.test(email)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
validToken(token) {
|
|
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3231-token-authenticated-registration.md
|
|
if (/^[A-Za-z0-9._~-]{1,64}$/.test(token)) {
|
|
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'))
|
|
});
|
|
}
|
|
});
|
|
|
|
case "m.login.registration_token": {
|
|
this.step = steps.CREATING;
|
|
return new Promise((resolve, reject) => {
|
|
if (this.$config.registrationToken) {
|
|
// We have a token in config, use that!
|
|
//
|
|
const data = {
|
|
session: authData.session,
|
|
type: nextStage,
|
|
token: this.$config.registrationToken
|
|
};
|
|
submitStageData(resolve, reject, data);
|
|
} else {
|
|
this.step = steps.ENTER_TOKEN;
|
|
this.onTokenEntered = (token) => {
|
|
this.step = steps.CREATING;
|
|
const data = {
|
|
session: authData.session,
|
|
type: nextStage,
|
|
token: token
|
|
};
|
|
submitStageData(resolve, reject, data);
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
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">
|
|
@use "@/assets/css/chat.scss" as *;
|
|
</style>
|