build: switch from yarn to npm to manage js dependencies and move js contents to root

yarn v1 is being deprecated and starts to have some issues

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

View File

@@ -0,0 +1,59 @@
<template>
<section class="container mx-auto">
<h1 class="title" v-if="loading">
{{ t("Your email is being changed") }}
</h1>
<div v-else>
<div v-if="failed">
<o-notification
:title="t('Error while changing email')"
variant="danger"
>
{{
t(
"Either the email has already been changed, either the validation token is incorrect."
)
}}
</o-notification>
</div>
<h1 class="title" v-else>{{ t("Your email has been changed") }}</h1>
</div>
</section>
</template>
<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";
// metaInfo() {
// return {
// title: this.t("Validating email") as string,
// };
// },
const props = defineProps<{
token: string;
}>();
const loading = ref(true);
const failed = ref(false);
const router = useRouter();
const { t } = useI18n({ useScope: "global" });
onBeforeMount(() => validateEmail({ token: props.token }));
const { mutate: validateEmail, onDone, onError } = useMutation(VALIDATE_EMAIL);
onDone(async () => {
loading.value = false;
await router.push({ name: RouteName.HOME });
});
onError((err) => {
loading.value = false;
console.error(err);
failed.value = true;
});
</script>

View File

@@ -0,0 +1,313 @@
<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" v-if="config?.auth?.databaseLogin">
<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="text-center my-2">
<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="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="canRegister">
<o-button
tag="router-link"
variant="text"
:to="{
name: RouteName.REGISTER,
query: {
default_email: credentials.email,
default_password: credentials.password,
},
}"
>{{ t("Create an account") }}</o-button
>
</p>
</div>
</form>
<div v-if="config && config?.auth?.oauthProviders?.length > 0">
<auth-providers :oauthProviders="config.auth.oauthProviders" />
</div>
</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";
import { useHead } from "@vueuse/head";
const { t } = useI18n({ useScope: "global" });
const router = useRouter();
const route = useRoute();
const { currentUser } = useCurrentUserClient();
const { result: configResult } = useQuery<{
config: Pick<
IConfig,
"auth" | "registrationsOpen" | "registrationsAllowlist"
>;
}>(LOGIN_CONFIG);
const config = computed(() => configResult.value?.config);
const canRegister = computed(() => {
return (
(config.value?.registrationsOpen || config.value?.registrationsAllowlist) &&
config.value?.auth?.databaseLogin
);
});
const errors = ref<string[]>([]);
const submitted = ref(false);
const credentials = reactive({
email: typeof route.query.email === "string" ? route.query.email : "",
password:
typeof route.query.password === "string" ? route.query.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;
}
console.debug("No redirect, going to homepage");
if (window.localStorage) {
console.debug("Has localstorage, setting welcome back");
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;
errors.value = [];
loginMutation({
email: credentials.email,
password: credentials.password,
});
};
const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
useMutation(UPDATE_CURRENT_USER_CLIENT);
onCurrentUserMutationDone(async () => {
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",
},
});
} else {
throw err;
}
}
});
const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
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 "warning";
}
return undefined;
});
const currentProvider = computed(() => {
const queryProvider = route?.query.provider as string | undefined;
if (queryProvider) {
return SELECTED_PROVIDERS[queryProvider];
}
return "unknown provider";
});
onMounted(() => {
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("/");
}
});
useHead({
title: computed(() => t("Login")),
});
</script>

View File

@@ -0,0 +1,115 @@
<template>
<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" 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";
const props = defineProps<{ token: string }>();
const { t } = useI18n({ useScope: "global" });
useHead({ title: computed(() => t("Password reset")) });
const credentials = reactive<{
password: string;
passwordConfirmation: string;
}>({
password: "",
passwordConfirmation: "",
});
const errors = ref<string[]>([]);
// 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");
}
saveUserData(data.resetPassword);
router.push({ name: RouteName.HOME });
return;
});
resetPasswordMutationError((err) => {
err.graphQLErrors.forEach(({ message }: { message: any }) => {
errors.value.push(message);
});
});
const resetAction = (e: Event) => {
e.preventDefault();
errors.value.splice(0);
resetPasswordMutation({
password: credentials.password,
token: props.token,
});
};
</script>

View File

@@ -0,0 +1,86 @@
<template>
<p>{{ t("Redirecting in progress…") }}</p>
</template>
<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 } from "../../utils/auth";
import { changeIdentity } from "../../utils/identity";
import { ICurrentUser, IUser } from "../../types/current-user.model";
import { useRouter } from "vue-router";
import { useLazyQuery, useMutation } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
import { computed, onMounted } from "vue";
import { getValueFromMeta } from "@/utils/html";
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Redirecting to Mobilizon")),
});
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: onUpdateCurrentUserClientDone,
mutate: updateCurrentUserClient,
} = useMutation<
{ updateCurrentUser: ICurrentUser },
{ id: string; email: string; isLoggedIn: boolean; role: ICurrentUserRole }
>(UPDATE_CURRENT_USER_CLIENT);
const { load: loadUser } = useLazyQuery<{
loggedUser: IUser;
}>(LOGGED_USER);
onUpdateCurrentUserClientDone(async () => {
try {
const result = await loadUser();
if (!result) return;
const loggedUser = result.loggedUser;
if (loggedUser.defaultActor) {
await changeIdentity(loggedUser.defaultActor);
await router.push({ name: RouteName.HOME });
} else {
// No need to push to REGISTER_PROFILE, the navbar will do it for us
}
} catch (e) {
console.error(e);
}
});
onMounted(async () => {
if (!(userId && userEmail && userRole && accessToken && refreshToken)) {
await router.push("/");
} else {
const login = {
user: {
id: userId,
email: userEmail,
role: userRole,
isLoggedIn: true,
defaultActor: undefined,
actors: [],
},
accessToken,
refreshToken,
};
saveUserData(login);
updateCurrentUserClient({
id: userId,
email: userEmail,
isLoggedIn: true,
role: userRole,
});
}
});
</script>

View File

@@ -0,0 +1,340 @@
<template>
<div class="container mx-auto py-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 #instanceName>
<b>{{ config?.name }}</b>
</template>
<template #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 #fullRules>
<router-link class="out" :to="{ name: RouteName.RULES }">{{
t("full rules")
}}</router-link>
</template>
<template #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')"
:variant="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 mt-2">
<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 #instanceRules>
<router-link class="out" :to="{ name: RouteName.RULES }">{{
t("instance rules")
}}</router-link>
</template>
<template #termsOfService>
<router-link class="out" :to="{ name: RouteName.TERMS }">{{
t("terms of service")
}}</router-link>
</template>
</i18n-t>
</label>
</div>
<p>
<o-button
variant="primary"
size="large"
:disabled="sendingForm"
native-type="submit"
>
{{ t("Create an account") }}
</o-button>
</p>
<p class="my-6">
<o-button
tag="router-link"
variant="text"
:to="{
name: RouteName.RESEND_CONFIRMATION,
params: { email: credentials.email },
}"
>{{ t("Didn't receive the instructions?") }}</o-button
>
<o-button
tag="router-link"
variant="text"
:to="{
name: RouteName.LOGIN,
query: {
email: credentials.email,
password: credentials.password,
},
}"
>{{ t("Login") }}</o-button
>
</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 { computed, reactive, ref, watch } from "vue";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useHead } from "@vueuse/head";
import { AbsintheGraphQLErrors } from "@/types/errors.model";
type errorType = "danger" | "warning";
type errorMessage = { type: errorType; message: string };
type credentialsType = { email: string; password: string; locale: string };
const { t, locale } = useI18n({ useScope: "global" });
const route = useRoute();
const router = useRouter();
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
const config = computed(() => configResult.value?.config);
const showGravatar = ref(false);
const credentials = reactive<credentialsType>({
email: typeof route.query.email === "string" ? route.query.email : "",
password:
typeof route.query.password === "string" ? route.query.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,
});
}
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) => {
(error.graphQLErrors as AbsintheGraphQLErrors).forEach(
({ field, message }) => {
switch (field) {
case "email":
emailErrors.value.push({
type: "danger" as errorType,
message: message[0] as string,
});
break;
case "password":
passwordErrors.value.push({
type: "danger" as errorType,
message: message[0] as string,
});
break;
default:
}
}
);
sendingForm.value = false;
});
const submit = async (): Promise<void> => {
sendingForm.value = true;
credentials.locale = locale as unknown 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: "warning" as errorType,
message: t(
"Emails usually don't contain capitals, make sure you haven't made a typo."
),
};
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 === "danger" || acc === "danger") return "danger";
return "warning";
}, "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>

View File

@@ -0,0 +1,97 @@
<template>
<section class="container mx-auto pt-4 max-w-2xl">
<h1>
{{ $t("Resend confirmation email") }}
</h1>
<o-notification v-if="error" variant="danger">
{{ errorMessage }}
</o-notification>
<form v-if="!validationSent" @submit="resendConfirmationAction">
<o-field :label="$t('Email address')" labelFor="emailAddress">
<o-input
aria-required="true"
required
type="email"
id="emailAddress"
v-model="emailValue"
/>
</o-field>
<p class="flex flex-wrap gap-1 mt-2">
<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: emailValue }
)
}}
</o-notification>
<o-notification variant="info" class="mt-2">
{{
$t("Please check your spam folder if you didn't receive the email.")
}}
</o-notification>
</div>
</section>
</template>
<script lang="ts" setup>
import { RESEND_CONFIRMATION_EMAIL } from "@/graphql/auth";
import RouteName from "@/router/name";
import { ref, computed } from "vue";
import { useMutation } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Resend confirmation email")),
meta: [{ name: "robots", content: "noindex" }],
});
const props = withDefaults(defineProps<{ email: string }>(), { email: "" });
const defaultEmail = computed(() => props.email);
const emailValue = ref<string>(defaultEmail.value);
const validationSent = ref(false);
const error = ref(false);
const errorMessage = ref<string>();
const {
mutate: resendConfirmationEmail,
onDone: resentConfirmationEmail,
onError: resentConfirmationEmailError,
} = useMutation(RESEND_CONFIRMATION_EMAIL);
resentConfirmationEmail(() => {
validationSent.value = true;
});
resentConfirmationEmailError((err) => {
console.error(err);
error.value = true;
errorMessage.value = err.graphQLErrors[0].message;
});
const resendConfirmationAction = async (e: Event): Promise<void> => {
e.preventDefault();
error.value = false;
resendConfirmationEmail({
email: emailValue.value,
});
};
</script>

View File

@@ -0,0 +1,115 @@
<template>
<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="emailValue"
/>
</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: emailValue,
})
}}
</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" setup>
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
import RouteName from "../../router/name";
import { computed, ref } from "vue";
import { useMutation } from "@vue/apollo-composable";
import { useHead } from "@vueuse/head";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Reset password")),
});
const props = withDefaults(
defineProps<{
email?: string;
}>(),
{ email: "" }
);
const defaultEmail = computed(() => props.email);
const emailValue = ref<string>(defaultEmail.value);
const validationSent = ref(false);
const errors = ref<string[]>([]);
const removeError = (message: string): void => {
errors.value.splice(errors.value.indexOf(message));
};
const {
mutate: sendResetPasswordMutation,
onDone: sendResetPasswordDone,
onError: sendResetPasswordError,
} = useMutation(SEND_RESET_PASSWORD);
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);
}
});
}
});
const sendResetPasswordTokenAction = async (e: Event): Promise<void> => {
e.preventDefault();
sendResetPasswordMutation({
email: emailValue.value,
});
};
</script>

View File

@@ -0,0 +1,106 @@
<template>
<div class="container mx-auto">
<h1 class="title">{{ $t("Let's define a few settings") }}</h1>
<o-steps v-model="stepIndex" :has-navigation="false">
<o-step-item step="1" :label="$t('Settings')">
<settings-onboarding />
</o-step-item>
<o-step-item step="2" :label="$t('Participation notifications')">
<notifications-onboarding />
</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">
<o-button
outlined
:disabled="stepIndex < 1"
tag="router-link"
:to="{
name: RouteName.WELCOME_SCREEN,
params: { step: stepIndex },
}"
>
{{ $t("Previous") }}
</o-button>
<o-button
outlined
variant="success"
v-if="stepIndex < 2"
tag="router-link"
:to="{
name: RouteName.WELCOME_SCREEN,
params: { step: stepIndex + 2 },
}"
>
{{ $t("Next") }}
</o-button>
<o-button
v-if="stepIndex >= 2"
variant="success"
size="big"
tag="router-link"
:to="{ name: RouteName.HOME }"
>
{{ $t("All good, let's continue!") }}
</o-button>
</section>
</div>
</template>
<script lang="ts" setup>
import { USER_SETTINGS } from "@/graphql/user";
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";
const { currentActor } = useCurrentActorClient();
const { instanceName } = useInstanceName();
const { refetch } = useQuery<{ loggedUser: IUser }>(USER_SETTINGS);
const NotificationsOnboarding = defineAsyncComponent(
() => import("@/components/Settings/NotificationsOnboarding.vue")
);
const SettingsOnboarding = defineAsyncComponent(
() => import("@/components/Settings/SettingsOnboarding.vue")
);
const ProfileOnboarding = defineAsyncComponent(
() => import("@/components/Account/ProfileOnboarding.vue")
);
const props = withDefaults(
defineProps<{
step?: number;
}>(),
{
step: 1,
}
);
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 {
.has-text-centered.section.buttons {
justify-content: center;
}
}
</style>

View 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>