Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,62 +1,59 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<section class="container mx-auto">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t("Your email is being changed") }}
|
||||
{{ t("Your email is being changed") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<b-message :title="$t('Error while changing email')" type="is-danger">
|
||||
<o-notification
|
||||
:title="t('Error while changing email')"
|
||||
variant="danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Either the email has already been changed, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</o-notification>
|
||||
</div>
|
||||
<h1 class="title" v-else>{{ $t("Your email has been changed") }}</h1>
|
||||
<h1 class="title" v-else>{{ t("Your email has been changed") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { ref, onBeforeMount } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { VALIDATE_EMAIL } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Validating email") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Validate extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
// metaInfo() {
|
||||
// return {
|
||||
// title: this.t("Validating email") as string,
|
||||
// };
|
||||
// },
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
loading = true;
|
||||
const loading = ref(true);
|
||||
const failed = ref(false);
|
||||
const router = useRouter();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
failed = false;
|
||||
onBeforeMount(() => validateEmail({ token: props.token }));
|
||||
|
||||
async created(): Promise<void> {
|
||||
await this.validateAction();
|
||||
}
|
||||
const { mutate: validateEmail, onDone, onError } = useMutation(VALIDATE_EMAIL);
|
||||
|
||||
async validateAction(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate<{ validateEmail: ICurrentUser }>({
|
||||
mutation: VALIDATE_EMAIL,
|
||||
variables: {
|
||||
token: this.token,
|
||||
},
|
||||
});
|
||||
this.loading = false;
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
this.loading = false;
|
||||
console.error(err);
|
||||
this.failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
onDone(async () => {
|
||||
loading.value = false;
|
||||
await router.push({ name: RouteName.HOME });
|
||||
});
|
||||
onError((err) => {
|
||||
loading.value = false;
|
||||
console.error(err);
|
||||
failed.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
<template>
|
||||
<section class="section container" v-if="!currentUser.isLoggedIn">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">{{ $t("Welcome back!") }}</h1>
|
||||
<b-message
|
||||
v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN"
|
||||
title="Info"
|
||||
type="is-info"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{ $t("You need to login.") }}</b-message
|
||||
>
|
||||
<b-message
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_ERROR"
|
||||
type="is-danger"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{
|
||||
$t(
|
||||
"Error while login with {provider}. Retry or login another way.",
|
||||
{
|
||||
provider:
|
||||
SELECTED_PROVIDERS[$route.query.provider] ||
|
||||
"unknown provider",
|
||||
}
|
||||
)
|
||||
}}</b-message
|
||||
>
|
||||
<b-message
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_NOT_FOUND"
|
||||
type="is-danger"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{
|
||||
$t(
|
||||
"Error while login with {provider}. This login provider doesn't exist.",
|
||||
{
|
||||
provider:
|
||||
SELECTED_PROVIDERS[$route.query.provider] ||
|
||||
"unknown provider",
|
||||
}
|
||||
)
|
||||
}}</b-message
|
||||
>
|
||||
<b-message
|
||||
:title="$t('Error')"
|
||||
type="is-danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<form @submit="loginAction">
|
||||
<b-field
|
||||
:label="$t('Email')"
|
||||
label-for="email"
|
||||
:message="caseWarningText"
|
||||
:type="caseWarningType"
|
||||
>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Password')" label-for="password">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
id="password"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<p class="control has-text-centered" v-if="!submitted">
|
||||
<button type="submit" class="button is-primary is-large">
|
||||
{{ $t("Login") }}
|
||||
</button>
|
||||
</p>
|
||||
<b-loading :is-full-page="false" v-model="submitted" />
|
||||
|
||||
<div
|
||||
class="control"
|
||||
v-if="config && config.auth.oauthProviders.length > 0"
|
||||
>
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.SEND_PASSWORD_RESET,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ $t("Forgot your password ?") }}</router-link
|
||||
>
|
||||
</p>
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.RESEND_CONFIRMATION,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ $t("Didn't receive the instructions?") }}</router-link
|
||||
>
|
||||
<p class="control" v-if="config && config.registrationsOpen">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.REGISTER,
|
||||
params: {
|
||||
default_email: credentials.email,
|
||||
default_password: credentials.password,
|
||||
},
|
||||
}"
|
||||
>{{ $t("Create an account") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
import { LOGIN } from "../../graphql/auth";
|
||||
import {
|
||||
validateEmailField,
|
||||
validateRequiredField,
|
||||
} from "../../utils/validators";
|
||||
import {
|
||||
initializeCurrentActor,
|
||||
NoIdentitiesException,
|
||||
saveUserData,
|
||||
SELECTED_PROVIDERS,
|
||||
} from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import {
|
||||
CURRENT_USER_CLIENT,
|
||||
UPDATE_CURRENT_USER_CLIENT,
|
||||
} from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
config: {
|
||||
query: CONFIG,
|
||||
},
|
||||
currentUser: {
|
||||
query: CURRENT_USER_CLIENT,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
AuthProviders,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Login on Mobilizon!") as string,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||
|
||||
LoginErrorCode = LoginErrorCode;
|
||||
|
||||
LoginError = LoginError;
|
||||
|
||||
errorCode: LoginErrorCode | null = null;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
credentials = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
redirect: string | undefined = "";
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
rules = {
|
||||
required: validateRequiredField,
|
||||
email: validateEmailField,
|
||||
};
|
||||
|
||||
SELECTED_PROVIDERS = SELECTED_PROVIDERS;
|
||||
|
||||
submitted = false;
|
||||
|
||||
mounted(): void {
|
||||
this.credentials.email = this.email;
|
||||
this.credentials.password = this.password;
|
||||
|
||||
const { query } = this.$route;
|
||||
this.errorCode = query.code as LoginErrorCode;
|
||||
this.redirect = query.redirect as string | undefined;
|
||||
|
||||
// Already-logged-in and accessing /login
|
||||
if (this.currentUser.isLoggedIn) {
|
||||
this.$router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
async loginAction(e: Event): Promise<Route | void> {
|
||||
e.preventDefault();
|
||||
if (this.submitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.errors = [];
|
||||
|
||||
try {
|
||||
this.submitted = true;
|
||||
const { data } = await this.$apollo.mutate<{ login: ILogin }>({
|
||||
mutation: LOGIN,
|
||||
variables: {
|
||||
email: this.credentials.email,
|
||||
password: this.credentials.password,
|
||||
},
|
||||
});
|
||||
if (data == null) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
saveUserData(data.login);
|
||||
await this.setupClientUserAndActors(data.login);
|
||||
|
||||
if (this.redirect) {
|
||||
this.$router.push(this.redirect as string);
|
||||
return;
|
||||
}
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
}
|
||||
this.$router.replace({ name: RouteName.HOME });
|
||||
return;
|
||||
} catch (err: any) {
|
||||
this.submitted = false;
|
||||
if (err.graphQLErrors) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
this.errors.push(message);
|
||||
});
|
||||
} else if (err.networkError) {
|
||||
this.errors.push(err.networkError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async setupClientUserAndActors(login: ILogin): Promise<void> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
variables: {
|
||||
id: login.user.id,
|
||||
email: this.credentials.email,
|
||||
isLoggedIn: true,
|
||||
role: login.user.role,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
||||
} catch (err: any) {
|
||||
if (err instanceof NoIdentitiesException) {
|
||||
await this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: {
|
||||
email: this.currentUser.email,
|
||||
userAlreadyActivated: "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasCaseWarning(): boolean {
|
||||
return this.credentials.email !== this.credentials.email.toLowerCase();
|
||||
}
|
||||
|
||||
get caseWarningText(): string | undefined {
|
||||
if (this.hasCaseWarning) {
|
||||
return this.$t(
|
||||
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||
) as string;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get caseWarningType(): string | undefined {
|
||||
if (this.hasCaseWarning) {
|
||||
return "is-warning";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
::v-deep .help.is-warning {
|
||||
color: #755033;
|
||||
}
|
||||
</style>
|
||||
309
js/src/views/User/LoginView.vue
Normal file
309
js/src/views/User/LoginView.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<section
|
||||
class="container mx-auto max-w-screen-sm"
|
||||
v-if="!currentUser?.isLoggedIn"
|
||||
>
|
||||
<h1 class="text-4xl">{{ t("Welcome back!") }}</h1>
|
||||
<o-notification
|
||||
v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN"
|
||||
title="Info"
|
||||
variant="info"
|
||||
:aria-close-label="t('Close')"
|
||||
>{{ t("You need to login.") }}</o-notification
|
||||
>
|
||||
<o-notification
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_ERROR"
|
||||
variant="danger"
|
||||
:aria-close-label="t('Close')"
|
||||
>{{
|
||||
t("Error while login with {provider}. Retry or login another way.", {
|
||||
provider: currentProvider,
|
||||
})
|
||||
}}</o-notification
|
||||
>
|
||||
<o-notification
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_NOT_FOUND"
|
||||
variant="danger"
|
||||
:aria-close-label="t('Close')"
|
||||
>{{
|
||||
t(
|
||||
"Error while login with {provider}. This login provider doesn't exist.",
|
||||
{
|
||||
provider: currentProvider,
|
||||
}
|
||||
)
|
||||
}}</o-notification
|
||||
>
|
||||
<o-notification
|
||||
:title="t('Error')"
|
||||
variant="danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<form @submit="loginAction">
|
||||
<o-field
|
||||
:label="t('Email')"
|
||||
label-for="email"
|
||||
:message="caseWarningText"
|
||||
:type="caseWarningType"
|
||||
>
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-field :label="t('Password')" label-for="password">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
id="password"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<p class="control has-text-centered" v-if="!submitted">
|
||||
<o-button
|
||||
variant="primary"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:disabled="submitted"
|
||||
>
|
||||
{{ t("Login") }}
|
||||
</o-button>
|
||||
</p>
|
||||
<!-- <o-loading :is-full-page="false" v-model="submitted" /> -->
|
||||
|
||||
<div
|
||||
class="control"
|
||||
v-if="config && config?.auth.oauthProviders.length > 0"
|
||||
>
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="text"
|
||||
:to="{
|
||||
name: RouteName.SEND_PASSWORD_RESET,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ t("Forgot your password ?") }}</o-button
|
||||
>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="text"
|
||||
:to="{
|
||||
name: RouteName.RESEND_CONFIRMATION,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ t("Didn't receive the instructions?") }}</o-button
|
||||
>
|
||||
<p class="control" v-if="config && config.registrationsOpen">
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="text"
|
||||
:to="{
|
||||
name: RouteName.REGISTER,
|
||||
params: {
|
||||
default_email: credentials.email,
|
||||
default_password: credentials.password,
|
||||
},
|
||||
}"
|
||||
>{{ t("Create an account") }}</o-button
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LOGIN } from "@/graphql/auth";
|
||||
import { LOGIN_CONFIG } from "@/graphql/config";
|
||||
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { ILogin } from "@/types/login.model";
|
||||
import { saveUserData, SELECTED_PROVIDERS } from "@/utils/auth";
|
||||
import {
|
||||
initializeCurrentActor,
|
||||
NoIdentitiesException,
|
||||
} from "@/utils/identity";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, reactive, ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import AuthProviders from "@/components/User/AuthProviders.vue";
|
||||
import RouteName from "@/router/name";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
}>(),
|
||||
{ email: "", password: "" }
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
|
||||
const { result: configResult } = useQuery<{
|
||||
config: Pick<IConfig, "auth" | "registrationsOpen">;
|
||||
}>(LOGIN_CONFIG);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const errors = ref<string[]>([]);
|
||||
const submitted = ref(false);
|
||||
|
||||
const credentials = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const redirect = ref<string | undefined>("");
|
||||
|
||||
const errorCode = ref<LoginErrorCode | null>(null);
|
||||
|
||||
const {
|
||||
onDone: onLoginMutationDone,
|
||||
onError: onLoginMutationError,
|
||||
mutate: loginMutation,
|
||||
} = useMutation(LOGIN);
|
||||
|
||||
onLoginMutationDone(async (result) => {
|
||||
const data = result.data;
|
||||
submitted.value = false;
|
||||
if (data == null) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
saveUserData(data.login);
|
||||
await setupClientUserAndActors(data.login);
|
||||
|
||||
if (redirect.value) {
|
||||
router.push(redirect.value);
|
||||
return;
|
||||
}
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
}
|
||||
router.replace({ name: RouteName.HOME });
|
||||
return;
|
||||
});
|
||||
|
||||
onLoginMutationError((err) => {
|
||||
console.error(err);
|
||||
submitted.value = false;
|
||||
if (err.graphQLErrors) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
errors.value.push(message);
|
||||
});
|
||||
} else if (err.networkError) {
|
||||
errors.value.push(err.networkError.message);
|
||||
}
|
||||
});
|
||||
|
||||
const loginAction = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (submitted.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitted.value = true;
|
||||
|
||||
loginMutation({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
};
|
||||
|
||||
const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
|
||||
useMutation(UPDATE_CURRENT_USER_CLIENT);
|
||||
|
||||
onCurrentUserMutationDone(async () => {
|
||||
console.debug("saved current user client, now for actor client");
|
||||
try {
|
||||
await initializeCurrentActor();
|
||||
} catch (err: any) {
|
||||
if (err instanceof NoIdentitiesException && currentUser.value) {
|
||||
await router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: {
|
||||
email: currentUser.value.email,
|
||||
userAlreadyActivated: "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
|
||||
console.debug("setuping client user and actors after login", login);
|
||||
updateCurrentUserMutation({
|
||||
id: login.user.id,
|
||||
email: credentials.email,
|
||||
isLoggedIn: true,
|
||||
role: login.user.role,
|
||||
});
|
||||
};
|
||||
|
||||
const hasCaseWarning = computed<boolean>(() => {
|
||||
return credentials.email !== credentials.email.toLowerCase();
|
||||
});
|
||||
|
||||
const caseWarningText = computed<string | undefined>(() => {
|
||||
if (hasCaseWarning.value) {
|
||||
return t(
|
||||
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||
) as string;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const caseWarningType = computed<string | undefined>(() => {
|
||||
if (hasCaseWarning.value) {
|
||||
return "is-warning";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const currentProvider = computed(() => {
|
||||
const queryProvider = route?.query.provider as string | undefined;
|
||||
if (queryProvider) {
|
||||
return SELECTED_PROVIDERS[queryProvider];
|
||||
}
|
||||
return "unknown provider";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
credentials.email = props.email;
|
||||
credentials.password = props.password;
|
||||
|
||||
const query = route?.query;
|
||||
errorCode.value = query?.code as LoginErrorCode;
|
||||
redirect.value = query?.redirect as string | undefined;
|
||||
|
||||
// Already-logged-in and accessing /login
|
||||
if (currentUser.value?.isLoggedIn) {
|
||||
console.debug(
|
||||
"Current user is already logged-in, redirecting to Homepage",
|
||||
currentUser
|
||||
);
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,120 +1,115 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t("Password reset") }}
|
||||
</h1>
|
||||
<b-message
|
||||
:title="$t('Error')"
|
||||
type="is-danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>{{ error }}</b-message
|
||||
>
|
||||
<form @submit="resetAction">
|
||||
<b-field :label="$t('Password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field :label="$t('Password (confirmation)')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password_confirmation"
|
||||
/>
|
||||
</b-field>
|
||||
<button class="button is-primary">
|
||||
{{ $t("Reset my password") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<section class="container mx-auto">
|
||||
<h1 class="">
|
||||
{{ $t("Password reset") }}
|
||||
</h1>
|
||||
<o-notification
|
||||
:title="$t('Error')"
|
||||
variant="danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
>{{ error }}</o-notification
|
||||
>
|
||||
<form @submit="resetAction">
|
||||
<o-field :label="$t('Password')">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</o-field>
|
||||
<o-field :label="$t('Password (confirmation)')">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.passwordConfirmation"
|
||||
/>
|
||||
</o-field>
|
||||
<button class="button is-primary">
|
||||
{{ $t("Reset my password") }}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { validateRequiredField } from "../../utils/validators";
|
||||
import { RESET_PASSWORD } from "../../graphql/auth";
|
||||
import { saveUserData } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import RouteName from "../../router/name";
|
||||
<script lang="ts" setup>
|
||||
import { RESET_PASSWORD } from "@/graphql/auth";
|
||||
import { saveUserData } from "@/utils/auth";
|
||||
import { ILogin } from "@/types/login.model";
|
||||
import RouteName from "@/router/name";
|
||||
import { reactive, ref, computed } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Password reset") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class PasswordReset extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
const props = defineProps<{ token: string }>();
|
||||
|
||||
credentials = {
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
} as { password: string; passwordConfirmation: string };
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({ title: computed(() => t("Password reset")) });
|
||||
|
||||
errors: string[] = [];
|
||||
const credentials = reactive<{
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}>({
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
});
|
||||
|
||||
rules = {
|
||||
passwordLength: (value: string): boolean | string =>
|
||||
value.length > 6 || "Password must be at least 6 characters long",
|
||||
required: validateRequiredField,
|
||||
passwordEqual: (value: string): boolean | string =>
|
||||
value === this.credentials.password || "Passwords must be the same",
|
||||
};
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
get samePasswords(): boolean {
|
||||
return (
|
||||
this.rules.passwordLength(this.credentials.password) === true &&
|
||||
this.credentials.password === this.credentials.passwordConfirmation
|
||||
);
|
||||
// rules = {
|
||||
// passwordLength: (value: string): boolean | string =>
|
||||
// value.length > 6 || "Password must be at least 6 characters long",
|
||||
// required: validateRequiredField,
|
||||
// passwordEqual: (value: string): boolean | string =>
|
||||
// value === this.credentials.password || "Passwords must be the same",
|
||||
// };
|
||||
|
||||
// get samePasswords(): boolean {
|
||||
// return (
|
||||
// this.rules.passwordLength(this.credentials.password) === true &&
|
||||
// this.credentials.password === this.credentials.passwordConfirmation
|
||||
// );
|
||||
// }
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
mutate: resetPasswordMutation,
|
||||
onDone: resetPasswordMutationDone,
|
||||
onError: resetPasswordMutationError,
|
||||
} = useMutation<{ resetPassword: ILogin }>(RESET_PASSWORD);
|
||||
|
||||
resetPasswordMutationDone(({ data }) => {
|
||||
if (data == null) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
async resetAction(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
this.errors.splice(0);
|
||||
saveUserData(data.resetPassword);
|
||||
router.push({ name: RouteName.HOME });
|
||||
return;
|
||||
});
|
||||
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ resetPassword: ILogin }>({
|
||||
mutation: RESET_PASSWORD,
|
||||
variables: {
|
||||
password: this.credentials.password,
|
||||
token: this.token,
|
||||
},
|
||||
});
|
||||
if (data == null) {
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
resetPasswordMutationError((err) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: any }) => {
|
||||
errors.value.push(message);
|
||||
});
|
||||
});
|
||||
|
||||
saveUserData(data.resetPassword);
|
||||
this.$router.push({ name: RouteName.HOME });
|
||||
return;
|
||||
} catch (err: any) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: any }) => {
|
||||
this.errors.push(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const resetAction = (e: Event) => {
|
||||
e.preventDefault();
|
||||
errors.value.splice(0);
|
||||
|
||||
resetPasswordMutation({
|
||||
password: credentials.password,
|
||||
token: props.token,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
section.section.container {
|
||||
background: $white;
|
||||
}
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,79 @@
|
||||
<template>
|
||||
<p>{{ $t("Redirecting in progress…") }}</p>
|
||||
<p>{{ t("Redirecting in progress…") }}</p>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { UPDATE_CURRENT_USER_CLIENT, LOGGED_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, changeIdentity } from "../../utils/auth";
|
||||
import { IUser } from "../../types/current-user.model";
|
||||
import { saveUserData } from "../../utils/auth";
|
||||
import { changeIdentity } from "../../utils/identity";
|
||||
import { ICurrentUser, IUser } from "../../types/current-user.model";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed } from "vue";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Redirecting to Mobilizon") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ProviderValidate extends Vue {
|
||||
async mounted(): Promise<void> {
|
||||
const accessToken = this.getValueFromMeta("auth-access-token");
|
||||
const refreshToken = this.getValueFromMeta("auth-refresh-token");
|
||||
const userId = this.getValueFromMeta("auth-user-id");
|
||||
const userEmail = this.getValueFromMeta("auth-user-email");
|
||||
const userRole = this.getValueFromMeta(
|
||||
"auth-user-role"
|
||||
) as ICurrentUserRole;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Redirecting to Mobilizon")),
|
||||
});
|
||||
|
||||
if (!(userId && userEmail && userRole && accessToken && refreshToken)) {
|
||||
await this.$router.push("/");
|
||||
const getValueFromMeta = (name: string): string | null => {
|
||||
const element = document.querySelector(`meta[name="${name}"]`);
|
||||
if (element && element.getAttribute("content")) {
|
||||
return element.getAttribute("content");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const accessToken = getValueFromMeta("auth-access-token");
|
||||
const refreshToken = getValueFromMeta("auth-refresh-token");
|
||||
const userId = getValueFromMeta("auth-user-id");
|
||||
const userEmail = getValueFromMeta("auth-user-email");
|
||||
const userRole = getValueFromMeta("auth-user-role") as ICurrentUserRole;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { onDone, mutate } = useMutation<{ updateCurrentUser: ICurrentUser }>(
|
||||
UPDATE_CURRENT_USER_CLIENT
|
||||
);
|
||||
|
||||
onDone(() => {
|
||||
const { onResult: onLoggedUserResult } = useQuery<{ loggedUser: IUser }>(
|
||||
LOGGED_USER
|
||||
);
|
||||
|
||||
onLoggedUserResult(async ({ data: { loggedUser } }) => {
|
||||
if (loggedUser.defaultActor) {
|
||||
await changeIdentity(loggedUser.defaultActor);
|
||||
await router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
const login = {
|
||||
user: {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
role: userRole,
|
||||
isLoggedIn: true,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
saveUserData(login);
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
variables: {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
isLoggedIn: true,
|
||||
role: userRole,
|
||||
},
|
||||
});
|
||||
const { data } = await this.$apollo.query<{ loggedUser: IUser }>({
|
||||
query: LOGGED_USER,
|
||||
});
|
||||
const { loggedUser } = data;
|
||||
|
||||
if (loggedUser.defaultActor) {
|
||||
await changeIdentity(
|
||||
this.$apollo.provider.defaultClient,
|
||||
loggedUser.defaultActor
|
||||
);
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// No need to push to REGISTER_PROFILE, the navbar will do it for us
|
||||
}
|
||||
// No need to push to REGISTER_PROFILE, the navbar will do it for us
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getValueFromMeta(name: string): string | null {
|
||||
const element = document.querySelector(`meta[name="${name}"]`);
|
||||
if (element && element.getAttribute("content")) {
|
||||
return element.getAttribute("content");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!(userId && userEmail && userRole && accessToken && refreshToken)) {
|
||||
await router.push("/");
|
||||
} else {
|
||||
const login = {
|
||||
user: {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
role: userRole,
|
||||
isLoggedIn: true,
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
saveUserData(login);
|
||||
|
||||
mutate({
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
isLoggedIn: true,
|
||||
role: userRole,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
<template>
|
||||
<div class="section container">
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<h1 class="title">
|
||||
{{
|
||||
$t("Register an account on {instanceName}!", {
|
||||
instanceName: config.name,
|
||||
})
|
||||
}}
|
||||
</h1>
|
||||
<i18n
|
||||
tag="p"
|
||||
path="{instanceName} is an instance of the {mobilizon} software."
|
||||
>
|
||||
<b slot="instanceName">{{ config.name }}</b>
|
||||
<a
|
||||
href="https://joinmobilizon.org"
|
||||
target="_blank"
|
||||
class="out"
|
||||
slot="mobilizon"
|
||||
>{{ $t("Mobilizon") }}</a
|
||||
>
|
||||
</i18n>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div>
|
||||
<subtitle>{{ $t("Why create an account?") }}</subtitle>
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li>{{ $t("To create and manage your events") }}</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
"To create and manage multiples identities from a same account"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
"To register for an event by choosing one of your identities"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="config.features.groups">
|
||||
{{
|
||||
$t(
|
||||
"To create or join an group and start organizing with other people"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="config.features.groups">
|
||||
{{
|
||||
$t(
|
||||
"To follow groups and be informed of their latest events"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-link class="out" :to="{ name: RouteName.ABOUT }">{{
|
||||
$t("Learn more")
|
||||
}}</router-link>
|
||||
<hr role="presentation" />
|
||||
<div class="content">
|
||||
<subtitle>{{
|
||||
$t("About {instance}", { instance: config.name })
|
||||
}}</subtitle>
|
||||
<div class="content" v-html="config.description"></div>
|
||||
<i18n
|
||||
path="Please read the {fullRules} published by {instance}'s administrators."
|
||||
tag="p"
|
||||
>
|
||||
<router-link slot="fullRules" :to="{ name: RouteName.RULES }">{{
|
||||
$t("full rules")
|
||||
}}</router-link>
|
||||
<b slot="instance">{{ config.name }}</b>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-message type="is-warning" v-if="config.registrationsAllowlist">
|
||||
{{ $t("Registrations are restricted by allowlisting.") }}
|
||||
</b-message>
|
||||
<form v-on:submit.prevent="submit()">
|
||||
<b-field
|
||||
:label="$t('Email')"
|
||||
:type="errorEmailType"
|
||||
:message="errorEmailMessages"
|
||||
label-for="email"
|
||||
>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
@blur="showGravatar = true"
|
||||
@focus="showGravatar = false"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field
|
||||
:label="$t('Password')"
|
||||
:type="errorPasswordType"
|
||||
:message="errorPasswordMessages"
|
||||
label-for="password"
|
||||
>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-checkbox required>
|
||||
<i18n
|
||||
tag="span"
|
||||
path="I agree to the {instanceRules} and {termsOfService}"
|
||||
>
|
||||
<router-link
|
||||
class="out"
|
||||
slot="instanceRules"
|
||||
:to="{ name: RouteName.RULES }"
|
||||
>{{ $t("instance rules") }}</router-link
|
||||
>
|
||||
<router-link
|
||||
class="out"
|
||||
slot="termsOfService"
|
||||
:to="{ name: RouteName.TERMS }"
|
||||
>{{ $t("terms of service") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</b-checkbox>
|
||||
|
||||
<p class="create-account control has-text-centered">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
size="is-large"
|
||||
:disabled="sendingForm"
|
||||
native-type="submit"
|
||||
>
|
||||
{{ $t("Create an account") }}
|
||||
</b-button>
|
||||
</p>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.RESEND_CONFIRMATION,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ $t("Didn't receive the instructions?") }}</router-link
|
||||
>
|
||||
</p>
|
||||
<p class="control has-text-centered">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.LOGIN,
|
||||
params: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
}"
|
||||
>{{ $t("Login") }}</router-link
|
||||
>
|
||||
</p>
|
||||
|
||||
<hr role="presentation" />
|
||||
<div
|
||||
class="control"
|
||||
v-if="config && config.auth.oauthProviders.length > 0"
|
||||
>
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { CREATE_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
import { AbsintheGraphQLError } from "../../types/apollo";
|
||||
|
||||
type errorType = "is-danger" | "is-warning";
|
||||
type errorMessage = { type: errorType; message: string };
|
||||
type credentials = { email: string; password: string; locale: string };
|
||||
|
||||
@Component({
|
||||
components: { Subtitle, AuthProviders },
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
title: this.title,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
},
|
||||
})
|
||||
export default class Register extends Vue {
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||
|
||||
credentials: credentials = {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
emailErrors: errorMessage[] = [];
|
||||
passwordErrors: errorMessage[] = [];
|
||||
|
||||
sendingForm = false;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
get title(): string {
|
||||
if (this.config) {
|
||||
return this.$t("Register an account on {instanceName}!", {
|
||||
instanceName: this.config.name,
|
||||
}) as string;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
this.sendingForm = true;
|
||||
this.credentials.locale = this.$i18n.locale;
|
||||
try {
|
||||
this.emailErrors = [];
|
||||
this.passwordErrors = [];
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_USER,
|
||||
variables: this.credentials,
|
||||
});
|
||||
|
||||
this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: this.credentials.email },
|
||||
});
|
||||
} catch (error: any) {
|
||||
error.graphQLErrors.forEach(
|
||||
({ field, message }: AbsintheGraphQLError) => {
|
||||
switch (field) {
|
||||
case "email":
|
||||
this.emailErrors.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
case "password":
|
||||
this.passwordErrors.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.sendingForm = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("credentials", { deep: true })
|
||||
watchCredentials(credentials: credentials): void {
|
||||
if (credentials.email !== credentials.email.toLowerCase()) {
|
||||
const error = {
|
||||
type: "is-warning" as errorType,
|
||||
message: this.$t(
|
||||
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||
) as string,
|
||||
};
|
||||
this.emailErrors = [error];
|
||||
this.$forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
maxErrorType(errors: errorMessage[]): errorType | undefined {
|
||||
if (!errors || errors.length === 0) return undefined;
|
||||
return errors.reduce<errorType>((acc, error) => {
|
||||
if (error.type === "is-danger" || acc === "is-danger") return "is-danger";
|
||||
return "is-warning";
|
||||
}, "is-warning");
|
||||
}
|
||||
|
||||
get errorEmailType(): errorType | undefined {
|
||||
return this.maxErrorType(this.emailErrors);
|
||||
}
|
||||
|
||||
get errorPasswordType(): errorType | undefined {
|
||||
return this.maxErrorType(this.passwordErrors);
|
||||
}
|
||||
|
||||
get errorEmailMessages(): string[] {
|
||||
return this.emailErrors.map(({ message }) => message);
|
||||
}
|
||||
|
||||
get errorPasswordMessages(): string[] {
|
||||
return this.passwordErrors?.map(({ message }) => message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-enter-active {
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
|
||||
.avatar-enter,
|
||||
.avatar-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.avatar-leave {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
|
||||
h2.title {
|
||||
color: $primary;
|
||||
font-size: 2.5rem;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $secondary;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.create-account {
|
||||
::v-deep button {
|
||||
margin: 1rem auto 2rem;
|
||||
}
|
||||
}
|
||||
::v-deep .help.is-warning {
|
||||
color: #755033;
|
||||
}
|
||||
</style>
|
||||
345
js/src/views/User/RegisterView.vue
Normal file
345
js/src/views/User/RegisterView.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="container mx-auto pt-6">
|
||||
<section class="">
|
||||
<h1>
|
||||
{{
|
||||
t("Register an account on {instanceName}!", {
|
||||
instanceName: config?.name,
|
||||
})
|
||||
}}
|
||||
</h1>
|
||||
<i18n-t
|
||||
tag="p"
|
||||
keypath="{instanceName} is an instance of the {mobilizon} software."
|
||||
>
|
||||
<template v-slot:instanceName>
|
||||
<b>{{ config?.name }}</b>
|
||||
</template>
|
||||
<template v-slot:mobilizon>
|
||||
<a href="https://joinmobilizon.org" target="_blank" class="out">{{
|
||||
t("Mobilizon")
|
||||
}}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</section>
|
||||
<section class="flex flex-wrap gap-6">
|
||||
<div class="">
|
||||
<div class="my-4">
|
||||
<h2 class="text-xl">{{ t("Why create an account?") }}</h2>
|
||||
<div class="prose dark:prose-invert">
|
||||
<ul>
|
||||
<li>{{ t("To create and manage your events") }}</li>
|
||||
<li>
|
||||
{{
|
||||
t(
|
||||
"To create and manage multiples identities from a same account"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
t(
|
||||
"To register for an event by choosing one of your identities"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="config?.features.groups">
|
||||
{{
|
||||
t(
|
||||
"To create or join an group and start organizing with other people"
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="config?.features.groups">
|
||||
{{
|
||||
t("To follow groups and be informed of their latest events")
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-link class="out block my-4" :to="{ name: RouteName.ABOUT }">{{
|
||||
t("Learn more")
|
||||
}}</router-link>
|
||||
<hr role="presentation" />
|
||||
<div class="my-4">
|
||||
<h2 class="text-xl">
|
||||
{{ t("About {instance}", { instance: config?.name }) }}
|
||||
</h2>
|
||||
<div
|
||||
class="prose dark:prose-invert"
|
||||
v-html="config?.description"
|
||||
></div>
|
||||
<i18n-t
|
||||
keypath="Please read the {fullRules} published by {instance}'s administrators."
|
||||
tag="p"
|
||||
><template v-slot:fullRules>
|
||||
<router-link class="out" :to="{ name: RouteName.RULES }">{{
|
||||
t("full rules")
|
||||
}}</router-link>
|
||||
</template>
|
||||
<template v-slot:instance>
|
||||
<b>{{ config?.name }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<o-notification variant="warning" v-if="config?.registrationsAllowlist">
|
||||
{{ t("Registrations are restricted by allowlisting.") }}
|
||||
</o-notification>
|
||||
<form @submit.prevent="submit">
|
||||
<o-field
|
||||
:label="t('Email')"
|
||||
:type="errorEmailType"
|
||||
:message="errorEmailMessage"
|
||||
label-for="email"
|
||||
>
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
@blur="showGravatar = true"
|
||||
@focus="showGravatar = false"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<o-field
|
||||
:label="t('Password')"
|
||||
:type="errorPasswordType"
|
||||
:message="errorPasswordMessage"
|
||||
label-for="password"
|
||||
>
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password"
|
||||
/>
|
||||
</o-field>
|
||||
|
||||
<div class="flex items-start mb-6">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="accept_rules_terms"
|
||||
class="w-4 h-4 bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="accept_rules_terms"
|
||||
class="ml-2 text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
<i18n-t
|
||||
tag="span"
|
||||
keypath="I agree to the {instanceRules} and {termsOfService}"
|
||||
>
|
||||
<template v-slot:instanceRules>
|
||||
<router-link class="out" :to="{ name: RouteName.RULES }">{{
|
||||
t("instance rules")
|
||||
}}</router-link>
|
||||
</template>
|
||||
<template v-slot:termsOfService>
|
||||
<router-link class="out" :to="{ name: RouteName.TERMS }">{{
|
||||
t("terms of service")
|
||||
}}</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="create-account control has-text-centered">
|
||||
<o-button
|
||||
variant="primary"
|
||||
size="large"
|
||||
:disabled="sendingForm"
|
||||
native-type="submit"
|
||||
>
|
||||
{{ t("Create an account") }}
|
||||
</o-button>
|
||||
</p>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.RESEND_CONFIRMATION,
|
||||
params: { email: credentials.email },
|
||||
}"
|
||||
>{{ t("Didn't receive the instructions?") }}</router-link
|
||||
>
|
||||
</p>
|
||||
<p class="control has-text-centered">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{
|
||||
name: RouteName.LOGIN,
|
||||
params: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
}"
|
||||
>{{ t("Login") }}</router-link
|
||||
>
|
||||
</p>
|
||||
|
||||
<hr role="presentation" />
|
||||
<div
|
||||
class="control"
|
||||
v-if="config && config.auth.oauthProviders.length > 0"
|
||||
>
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CREATE_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
import { AbsintheGraphQLError } from "../../types/apollo";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
type errorType = "is-danger" | "is-warning";
|
||||
type errorMessage = { type: errorType; message: string };
|
||||
type credentialsType = { email: string; password: string; locale: string };
|
||||
|
||||
const { t, locale } = useI18n({ useScope: "global" });
|
||||
const router = useRouter();
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const showGravatar = ref(false);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
}>(),
|
||||
{ email: "", password: "" }
|
||||
);
|
||||
|
||||
const credentials = reactive<credentialsType>({
|
||||
email: props.email,
|
||||
password: props.password,
|
||||
locale: "en",
|
||||
});
|
||||
|
||||
const emailErrors = ref<errorMessage[]>([]);
|
||||
const passwordErrors = ref<errorMessage[]>([]);
|
||||
|
||||
const sendingForm = ref(false);
|
||||
|
||||
const title = computed((): string => {
|
||||
if (config.value) {
|
||||
return t("Register an account on {instanceName}!", {
|
||||
instanceName: config.value?.name,
|
||||
}) as string;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: title.value,
|
||||
});
|
||||
|
||||
const { onDone, onError, mutate } = useMutation(CREATE_USER);
|
||||
|
||||
onDone(() => {
|
||||
router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: credentials.email },
|
||||
});
|
||||
});
|
||||
|
||||
onError((error) => {
|
||||
// @ts-ignore
|
||||
error.graphQLErrors.forEach(({ field, message }: AbsintheGraphQLError) => {
|
||||
switch (field) {
|
||||
case "email":
|
||||
emailErrors.value.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
case "password":
|
||||
passwordErrors.value.push({
|
||||
type: "is-danger" as errorType,
|
||||
message: message[0] as string,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
sendingForm.value = false;
|
||||
});
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
sendingForm.value = true;
|
||||
credentials.locale = locale.value as string;
|
||||
try {
|
||||
emailErrors.value = [];
|
||||
passwordErrors.value = [];
|
||||
|
||||
mutate(credentials);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
||||
sendingForm.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(credentials, () => {
|
||||
if (credentials.email !== credentials.email.toLowerCase()) {
|
||||
const error = {
|
||||
type: "is-warning" as errorType,
|
||||
message: t(
|
||||
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||
) as string,
|
||||
};
|
||||
emailErrors.value = [error];
|
||||
}
|
||||
});
|
||||
|
||||
const maxErrorType = (errors: errorMessage[]): errorType | undefined => {
|
||||
if (!errors || errors.length === 0) return undefined;
|
||||
return errors.reduce<errorType>((acc, error) => {
|
||||
if (error.type === "is-danger" || acc === "is-danger") return "is-danger";
|
||||
return "is-warning";
|
||||
}, "is-warning");
|
||||
};
|
||||
|
||||
const errorEmailType = computed((): errorType | undefined => {
|
||||
return maxErrorType(emailErrors.value);
|
||||
});
|
||||
|
||||
const errorPasswordType = computed((): errorType | undefined => {
|
||||
return maxErrorType(passwordErrors.value);
|
||||
});
|
||||
|
||||
const errorEmailMessage = computed((): string => {
|
||||
return emailErrors.value.map(({ message }) => message).join(" ");
|
||||
});
|
||||
|
||||
const errorPasswordMessage = computed((): string => {
|
||||
return passwordErrors.value?.map(({ message }) => message).join(" ");
|
||||
});
|
||||
</script>
|
||||
@@ -1,121 +1,92 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t("Resend confirmation email") }}
|
||||
</h1>
|
||||
<form v-if="!validationSent" @submit="resendConfirmationAction">
|
||||
<b-field :label="$t('Email address')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
{{ $t("Send the confirmation email again") }}
|
||||
</b-button>
|
||||
<router-link
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
class="button is-text"
|
||||
>{{ $t("Cancel") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<b-message type="is-success" :closable="false" title="Success">
|
||||
{{
|
||||
$t(
|
||||
"If an account with this email exists, we just sent another confirmation email to {email}",
|
||||
{ email: credentials.email }
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
<b-message type="is-info">
|
||||
{{
|
||||
$t(
|
||||
"Please check your spam folder if you didn't receive the email."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</div>
|
||||
</div>
|
||||
<section class="container mx-auto pt-4 max-w-2xl">
|
||||
<h1>
|
||||
{{ $t("Resend confirmation email") }}
|
||||
</h1>
|
||||
<form v-if="!validationSent" @submit="resendConfirmationAction">
|
||||
<o-field :label="$t('Email address')">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</o-field>
|
||||
<p class="flex flex-wrap gap-1">
|
||||
<o-button variant="primary" native-type="submit">
|
||||
{{ $t("Send the confirmation email again") }}
|
||||
</o-button>
|
||||
<o-button
|
||||
variant="primary"
|
||||
outlined
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
>{{ $t("Cancel") }}</o-button
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<o-notification variant="success" :closable="false" title="Success">
|
||||
{{
|
||||
$t(
|
||||
"If an account with this email exists, we just sent another confirmation email to {email}",
|
||||
{ email: credentials.email }
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
<o-notification variant="info">
|
||||
{{
|
||||
$t("Please check your spam folder if you didn't receive the email.")
|
||||
}}
|
||||
</o-notification>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import {
|
||||
validateEmailField,
|
||||
validateRequiredField,
|
||||
} from "../../utils/validators";
|
||||
import { RESEND_CONFIRMATION_EMAIL } from "../../graphql/auth";
|
||||
import RouteName from "../../router/name";
|
||||
<script lang="ts" setup>
|
||||
import { RESEND_CONFIRMATION_EMAIL } from "@/graphql/auth";
|
||||
import RouteName from "@/router/name";
|
||||
import { reactive, ref, computed } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Resend confirmation email") as string,
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ResendConfirmation extends Vue {
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Resend confirmation email")),
|
||||
meta: [{ name: "robots", content: "noindex" }],
|
||||
});
|
||||
|
||||
credentials = {
|
||||
email: "",
|
||||
};
|
||||
const props = withDefaults(defineProps<{ email: string }>(), { email: "" });
|
||||
|
||||
validationSent = false;
|
||||
const credentials = reactive({
|
||||
email: props.email,
|
||||
});
|
||||
|
||||
error = false;
|
||||
const validationSent = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
RouteName = RouteName;
|
||||
const {
|
||||
mutate: resendConfirmationEmail,
|
||||
onDone: resentConfirmationEmail,
|
||||
onError: resentConfirmationEmailError,
|
||||
} = useMutation(RESEND_CONFIRMATION_EMAIL);
|
||||
|
||||
state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: "",
|
||||
},
|
||||
};
|
||||
resentConfirmationEmail(() => {
|
||||
validationSent.value = true;
|
||||
});
|
||||
|
||||
rules = {
|
||||
required: validateRequiredField,
|
||||
email: validateEmailField,
|
||||
};
|
||||
resentConfirmationEmailError((err) => {
|
||||
console.error(err);
|
||||
error.value = true;
|
||||
});
|
||||
|
||||
mounted(): void {
|
||||
this.credentials.email = this.email;
|
||||
}
|
||||
const resendConfirmationAction = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
error.value = false;
|
||||
|
||||
async resendConfirmationAction(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
this.error = false;
|
||||
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: RESEND_CONFIRMATION_EMAIL,
|
||||
variables: {
|
||||
email: this.credentials.email,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.error = true;
|
||||
} finally {
|
||||
this.validationSent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
resendConfirmationEmail({
|
||||
email: credentials.email,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,153 +1,116 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t("Forgot your password?") }}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"Enter your email address below, and we'll email you instructions on how to change your password."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-message
|
||||
title="Error"
|
||||
type="is-danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
@close="removeError(error)"
|
||||
>
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
|
||||
<b-field :label="$t('Email address')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
{{ $t("Submit") }}
|
||||
</b-button>
|
||||
<router-link
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
class="button is-text"
|
||||
>{{ $t("Cancel") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<b-message type="is-success" :closable="false" title="Success">
|
||||
{{
|
||||
$t("We just sent an email to {email}", {
|
||||
email: credentials.email,
|
||||
})
|
||||
}}
|
||||
</b-message>
|
||||
<b-message type="is-info">
|
||||
{{
|
||||
$t(
|
||||
"Please check your spam folder if you didn't receive the email."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</div>
|
||||
</div>
|
||||
<section class="container mx-auto">
|
||||
<h1>
|
||||
{{ t("Forgot your password?") }}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
"Enter your email address below, and we'll email you instructions on how to change your password."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<o-notification
|
||||
title="Error"
|
||||
variant="danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
@close="removeError(error)"
|
||||
>
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
|
||||
<o-field :label="t('Email address')">
|
||||
<o-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</o-field>
|
||||
<p class="control">
|
||||
<o-button variant="primary" native-type="submit">
|
||||
{{ t("Submit") }}
|
||||
</o-button>
|
||||
<router-link :to="{ name: RouteName.LOGIN }" class="button is-text">{{
|
||||
t("Cancel")
|
||||
}}</router-link>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<o-notification variant="success" :closable="false" title="Success">
|
||||
{{
|
||||
t("We just sent an email to {email}", {
|
||||
email: credentials.email,
|
||||
})
|
||||
}}
|
||||
</o-notification>
|
||||
<o-notification variant="info">
|
||||
{{
|
||||
t("Please check your spam folder if you didn't receive the email.")
|
||||
}}
|
||||
</o-notification>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import {
|
||||
validateEmailField,
|
||||
validateRequiredField,
|
||||
} from "../../utils/validators";
|
||||
<script lang="ts" setup>
|
||||
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
|
||||
import RouteName from "../../router/name";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Reset password") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class SendPasswordReset extends Vue {
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Reset password")),
|
||||
});
|
||||
|
||||
credentials = {
|
||||
email: "",
|
||||
} as { email: string };
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
email?: string;
|
||||
}>(),
|
||||
{ email: "" }
|
||||
);
|
||||
|
||||
validationSent = false;
|
||||
const credentials = reactive<{ email: string }>({
|
||||
email: props.email,
|
||||
});
|
||||
|
||||
RouteName = RouteName;
|
||||
const validationSent = ref(false);
|
||||
|
||||
errors: string[] = [];
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: "",
|
||||
} as { status: boolean | null; msg: string },
|
||||
};
|
||||
const removeError = (message: string): void => {
|
||||
errors.value.splice(errors.value.indexOf(message));
|
||||
};
|
||||
|
||||
rules = {
|
||||
required: validateRequiredField,
|
||||
email: validateEmailField,
|
||||
};
|
||||
const {
|
||||
mutate: sendResetPasswordMutation,
|
||||
onDone: sendResetPasswordDone,
|
||||
onError: sendResetPasswordError,
|
||||
} = useMutation(SEND_RESET_PASSWORD);
|
||||
|
||||
mounted(): void {
|
||||
this.credentials.email = this.email;
|
||||
}
|
||||
|
||||
removeError(message: string): void {
|
||||
this.errors.splice(this.errors.indexOf(message));
|
||||
}
|
||||
|
||||
async sendResetPasswordTokenAction(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: SEND_RESET_PASSWORD,
|
||||
variables: {
|
||||
email: this.credentials.email,
|
||||
},
|
||||
});
|
||||
|
||||
this.validationSent = true;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (err.graphQLErrors) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
if (this.errors.indexOf(message) < 0) {
|
||||
this.errors.push(message);
|
||||
}
|
||||
});
|
||||
sendResetPasswordDone(() => {
|
||||
validationSent.value = true;
|
||||
});
|
||||
sendResetPasswordError((err) => {
|
||||
console.error(err);
|
||||
if (err.graphQLErrors) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
if (errors.value.indexOf(message) < 0) {
|
||||
errors.value.push(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resetState(): void {
|
||||
this.state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const sendResetPasswordTokenAction = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
sendResetPasswordMutation({
|
||||
email: credentials.email,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div class="section container">
|
||||
<div class="container mx-auto">
|
||||
<h1 class="title">{{ $t("Let's define a few settings") }}</h1>
|
||||
<b-steps v-model="stepIndex" :has-navigation="false">
|
||||
<b-step-item step="1" :label="$t('Settings')">
|
||||
<o-steps v-model="stepIndex" :has-navigation="false">
|
||||
<o-step-item step="1" :label="$t('Settings')">
|
||||
<settings-onboarding />
|
||||
</b-step-item>
|
||||
<b-step-item step="2" :label="$t('Participation notifications')">
|
||||
</o-step-item>
|
||||
<o-step-item step="2" :label="$t('Participation notifications')">
|
||||
<notifications-onboarding />
|
||||
</b-step-item>
|
||||
<b-step-item step="3" :label="$t('Profiles and federation')">
|
||||
<profile-onboarding />
|
||||
</b-step-item>
|
||||
</b-steps>
|
||||
</o-step-item>
|
||||
<o-step-item step="3" :label="$t('Profiles and federation')">
|
||||
<ProfileOnboarding
|
||||
v-if="currentActor && instanceName"
|
||||
:current-actor="currentActor"
|
||||
:instance-name="instanceName"
|
||||
/>
|
||||
</o-step-item>
|
||||
</o-steps>
|
||||
<section class="has-text-centered section buttons">
|
||||
<b-button
|
||||
<o-button
|
||||
outlined
|
||||
:disabled="stepIndex < 1"
|
||||
tag="router-link"
|
||||
@@ -23,10 +27,10 @@
|
||||
}"
|
||||
>
|
||||
{{ $t("Previous") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
</o-button>
|
||||
<o-button
|
||||
outlined
|
||||
type="is-success"
|
||||
variant="success"
|
||||
v-if="stepIndex < 2"
|
||||
tag="router-link"
|
||||
:to="{
|
||||
@@ -35,61 +39,63 @@
|
||||
}"
|
||||
>
|
||||
{{ $t("Next") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
</o-button>
|
||||
<o-button
|
||||
v-if="stepIndex >= 2"
|
||||
type="is-success"
|
||||
variant="success"
|
||||
size="is-big"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.HOME }"
|
||||
>
|
||||
{{ $t("All good, let's continue!") }}
|
||||
</b-button>
|
||||
</o-button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { USER_SETTINGS } from "@/graphql/user";
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { TIMEZONES } from "../../graphql/config";
|
||||
import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed, defineAsyncComponent, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RouteName from "@/router/name";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useInstanceName } from "@/composition/apollo/config";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
NotificationsOnboarding: () =>
|
||||
import("../../components/Settings/NotificationsOnboarding.vue"),
|
||||
SettingsOnboarding: () =>
|
||||
import("../../components/Settings/SettingsOnboarding.vue"),
|
||||
ProfileOnboarding: () =>
|
||||
import("../../components/Account/ProfileOnboarding.vue"),
|
||||
},
|
||||
apollo: {
|
||||
config: TIMEZONES,
|
||||
loggedUser: USER_SETTINGS,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("First steps") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class SettingsOnboard extends Vue {
|
||||
@Prop({ required: false, default: 1, type: Number }) step!: number;
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
const { instanceName } = useInstanceName();
|
||||
const { refetch } = useQuery<{ loggedUser: IUser }>(USER_SETTINGS);
|
||||
|
||||
config!: IConfig;
|
||||
const NotificationsOnboarding = defineAsyncComponent(
|
||||
() => import("@/components/Settings/NotificationsOnboarding.vue")
|
||||
);
|
||||
const SettingsOnboarding = defineAsyncComponent(
|
||||
() => import("@/components/Settings/SettingsOnboarding.vue")
|
||||
);
|
||||
const ProfileOnboarding = defineAsyncComponent(
|
||||
() => import("@/components/Account/ProfileOnboarding.vue")
|
||||
);
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get stepIndex(): number {
|
||||
return this.step - 1;
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
step?: number;
|
||||
}>(),
|
||||
{
|
||||
step: 1,
|
||||
}
|
||||
);
|
||||
|
||||
@Watch("stepIndex")
|
||||
refetchUserSettings() {
|
||||
this.$apollo.queries.loggedUser.refetch();
|
||||
}
|
||||
}
|
||||
const stepIndex = computed(() => props.step - 1);
|
||||
|
||||
watch(stepIndex, () => {
|
||||
refetch();
|
||||
});
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("First steps")),
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.section.container {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t("Your account is being validated") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<b-message
|
||||
:title="$t('Error while validating account')"
|
||||
type="is-danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Either the account is already validated, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</div>
|
||||
<h1 class="title" v-else>{{ $t("Your account has been validated") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
|
||||
@Component({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Validating account") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Validate extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
|
||||
loading = true;
|
||||
|
||||
failed = false;
|
||||
|
||||
async created(): Promise<void> {
|
||||
await this.validateAction();
|
||||
}
|
||||
|
||||
async validateAction(): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ validateUser: ILogin }>({
|
||||
mutation: VALIDATE_USER,
|
||||
variables: {
|
||||
token: this.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (data) {
|
||||
saveUserData(data.validateUser);
|
||||
saveTokenData(data.validateUser);
|
||||
|
||||
const { user } = data.validateUser;
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
variables: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
isLoggedIn: true,
|
||||
role: ICurrentUserRole.USER,
|
||||
},
|
||||
});
|
||||
|
||||
if (user.defaultActor) {
|
||||
await changeIdentity(
|
||||
this.$apollo.provider.defaultClient,
|
||||
user.defaultActor
|
||||
);
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// If the user didn't register any profile yet, let's create one for them
|
||||
await this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: user.email, userAlreadyActivated: "true" },
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.failed = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
js/src/views/User/ValidateUser.vue
Normal file
103
js/src/views/User/ValidateUser.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<section class="container mx-auto">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t("Your account is being validated") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<o-notification
|
||||
:title="$t('Error while validating account')"
|
||||
variant="danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Either the account is already validated, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
</div>
|
||||
<h1 class="title" v-else>{{ $t("Your account has been validated") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, saveTokenData } from "../../utils/auth";
|
||||
import { changeIdentity } from "../../utils/identity";
|
||||
import { ref, onBeforeMount, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Validating account")),
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
}>();
|
||||
|
||||
const loading = ref(true);
|
||||
const failed = ref(false);
|
||||
|
||||
onBeforeMount(() => {
|
||||
validateAction({ token: props.token });
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const user = ref<IUser | null>(null);
|
||||
|
||||
const {
|
||||
mutate: validateAction,
|
||||
onDone: onValidatingUserMutationDone,
|
||||
onError: onValidatingUserMutationError,
|
||||
} = useMutation(VALIDATE_USER);
|
||||
|
||||
const {
|
||||
onDone: onUpdatingCurrentUserClientDone,
|
||||
mutate: updateCurrentUserClient,
|
||||
} = useMutation(UPDATE_CURRENT_USER_CLIENT);
|
||||
|
||||
onUpdatingCurrentUserClientDone(async () => {
|
||||
if (user.value?.defaultActor) {
|
||||
await changeIdentity(user.value?.defaultActor);
|
||||
await router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// If the user didn't register any profile yet, let's create one for them
|
||||
await router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: user.value?.email, userAlreadyActivated: "true" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onValidatingUserMutationDone(async ({ data }) => {
|
||||
if (data) {
|
||||
saveUserData(data.validateUser);
|
||||
saveTokenData(data.validateUser);
|
||||
|
||||
const { user: validatedUser } = data.validateUser;
|
||||
user.value = validatedUser;
|
||||
|
||||
updateCurrentUserClient({
|
||||
id: validatedUser.id,
|
||||
email: validatedUser.email,
|
||||
isLoggedIn: true,
|
||||
role: ICurrentUserRole.USER,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onValidatingUserMutationError((error) => {
|
||||
console.error(error);
|
||||
failed.value = true;
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user