keanu-weblite/src/components/InteractiveAuth.vue
N-Pex b1d47748c8 Use SASS module system
Get rid of all the SASS warnings/errors when building.
2025-05-19 10:25:46 +02:00

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>