refactor: Remove the registerPerson GraphQL query

- The first profile is now created after the user email validation
- NavBar is correctly updated when a user is connected but without any profile
- Always fetch identities from the server at login (no cache)
- NoIdentitiesException is thrown again
- Refactor EditIdentity to create the first profile
- Translations updated
- Tests updated

Fixes #1762
This commit is contained in:
Massedil
2025-05-23 20:34:00 +02:00
committed by setop
parent a9cfcd9e9d
commit 8bb6b0b97c
51 changed files with 138 additions and 609 deletions

View File

@@ -1,233 +0,0 @@
<template>
<section class="container mx-auto max-w-screen-sm">
<h1 class="text-2xl" v-if="userAlreadyActivated">
{{ t("Congratulations, your account is now created!") }}
</h1>
<h1 class="text-2xl" v-else>
{{
t("Register an account on {instanceName}!", {
instanceName,
})
}}
</h1>
<p class="prose dark:prose-invert" v-if="userAlreadyActivated">
{{ t("Now, create your first profile:") }}
</p>
<form v-if="!validationSent" @submit.prevent="submit">
<o-notification variant="danger" v-if="errors.extra">
{{ errors.extra }}
</o-notification>
<o-field :label="t('Displayed nickname')" labelFor="identityName">
<o-input
aria-required="true"
required
v-model="identity.name"
id="identityName"
expanded
@update:modelValue="(value: string) => updateUsername(value)"
/>
</o-field>
<o-field
:label="t('Username')"
:variant="errors.preferred_username ? 'danger' : 'primary'"
:message="errors.preferred_username"
labelFor="identityPreferredUsername"
>
<o-field
:message="
t(
'Only alphanumeric lowercased characters and underscores are supported.'
)
"
>
<o-input
aria-required="true"
required
expanded
id="identityPreferredUsername"
v-model="identity.preferredUsername"
:variant="errors.preferred_username ? 'danger' : ''"
:validation-message="
identity.preferredUsername
? t(
'Only alphanumeric lowercased characters and underscores are supported.'
)
: null
"
/>
<p class="control">
<span class="button">@{{ host }}</span>
</p>
</o-field>
</o-field>
<p class="prose dark:prose-invert">
{{
t(
"This identifier is unique to your profile. It allows others to find you."
)
}}
</p>
<o-field :label="t('Short bio')" labelFor="identitySummary">
<o-input
type="textarea"
maxlength="100"
rows="2"
id="identitySummary"
v-model="identity.summary"
expanded
/>
</o-field>
<p class="prose dark:prose-invert">
{{
t(
"You will be able to add an avatar and set other options in your account settings."
)
}}
</p>
<p class="text-center">
<o-button
variant="primary"
size="large"
native-type="submit"
:disabled="sendingValidation"
>{{ t("Create my profile") }}</o-button
>
</p>
</form>
<div v-if="validationSent && !userAlreadyActivated">
<o-notification variant="success" :closable="false">
<h2 class="title">
{{
t("Your account is nearly ready, {username}", {
username: identity.name ?? identity.preferredUsername,
})
}}
</h2>
<i18n-t keypath="A validation email was sent to {email}" tag="p">
<template #email>
<code>{{ email }}</code>
</template>
</i18n-t>
<p>
{{
t(
"Before you can login, you need to click on the link inside it to validate your account."
)
}}
</p>
<o-button tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Back to homepage")
}}</o-button>
</o-notification>
</div>
</section>
</template>
<script lang="ts" setup>
import { Person } from "../../types/actor";
import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
import RouteName from "../../router/name";
import { changeIdentity } from "../../utils/identity";
import { useInstanceName } from "@/composition/apollo/config";
import { ref, computed, onBeforeMount } from "vue";
import { useRouter } from "vue-router";
import { registerAccount } from "@/composition/apollo/user";
import { convertToUsername } from "@/utils/username";
import { useI18n } from "vue-i18n";
import { useHead } from "@/utils/head";
import { getValueFromMeta } from "@/utils/html";
const props = withDefaults(
defineProps<{
email: string;
userAlreadyActivated?: boolean;
}>(),
{
userAlreadyActivated: false,
}
);
const { instanceName } = useInstanceName();
const router = useRouter();
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Register")),
});
const host: string = MOBILIZON_INSTANCE_HOST;
const errors = ref<Record<string, unknown>>({});
const validationSent = ref(false);
const sendingValidation = ref(false);
const identity = ref(new Person());
onBeforeMount(() => {
// Make sure no one goes to this page if we don't want to
if (!props.email) {
router.replace({ name: RouteName.PAGE_NOT_FOUND });
}
const username = getValueFromMeta("auth-user-suggested-actor-username");
const name = getValueFromMeta("auth-user-suggested-actor-name");
if (username) {
identity.value.preferredUsername = convertToUsername(username);
}
if (name) {
identity.value.name = name;
}
});
const updateUsername = (value: string) => {
identity.value.preferredUsername = convertToUsername(value);
};
const { onDone, onError, mutate } = registerAccount();
onDone(async ({ data }) => {
validationSent.value = true;
window.localStorage.setItem("new-registered-user", "yes");
if (data && props.userAlreadyActivated) {
await changeIdentity(data.registerPerson);
await router.push({ name: RouteName.HOME });
}
});
onError((err) => {
errors.value = err.graphQLErrors.reduce(
(acc: { [key: string]: string }, error: any) => {
acc[error.details ?? error.field ?? "extra"] = Array.isArray(
error.message
)
? (error.message as string[]).join(",")
: error.message;
return acc;
},
{}
);
console.error("Error while registering person", err);
console.error("Errors while registering person", errors);
sendingValidation.value = false;
});
const submit = async (): Promise<void> => {
sendingValidation.value = true;
errors.value = {};
mutate(
{ email: props.email, ...identity.value },
{ context: { userAlreadyActivated: props.userAlreadyActivated } }
);
};
</script>

View File

@@ -6,9 +6,13 @@
<span v-if="isUpdate" class="line-clamp-2">{{
displayName(identity)
}}</span>
<span v-else>{{ t("I create an identity") }}</span>
<span v-else>{{ t("Create a new profile") }}</span>
</h1>
<o-field horizontal :label="t('Avatar')">
<div v-if="identities?.length == 0">
{{ t("Congratulations, your account is now created!") }}
{{ t("Now, create your first profile:") }}
</div>
<o-field :label="t('Avatar')">
<picture-upload
v-model="avatarFile"
:defaultImage="identity.avatar"
@@ -16,11 +20,7 @@
/>
</o-field>
<o-field
horizontal
:label="t('Display name')"
label-for="identity-display-name"
>
<o-field :label="t('Display name')" label-for="identity-display-name">
<o-input
aria-required="true"
required
@@ -33,7 +33,6 @@
</o-field>
<o-field
horizontal
class="username-field"
:label="t('Username')"
label-for="identity-username"
@@ -59,11 +58,15 @@
</o-field>
</o-field>
<o-field
horizontal
:label="t('Description')"
label-for="identity-summary"
>
<p class="prose dark:prose-invert">
{{
t(
"This identifier is unique to your profile. It allows others to find you."
)
}}
</p>
<o-field :label="t('Description')" label-for="identity-summary">
<o-input
type="textarea"
dir="auto"
@@ -87,7 +90,7 @@
<o-field class="flex justify-center !my-6">
<div class="control">
<o-button type="button" variant="primary" @click="submit()">
{{ t("Save") }}
{{ t("Create my profile") }}
</o-button>
</div>
</o-field>
@@ -107,7 +110,9 @@
<p>
{{
t(
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings."
"These feeds contain event data for the events for which this specific profile is a participant or creator." +
"You should keep these private." +
" You can find feeds for all of your profiles into your notification settings."
)
}}
</p>
@@ -214,7 +219,10 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import pick from "lodash/pick";
import { ActorType } from "@/types/enums";
import { useRouter } from "vue-router";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import {
useCurrentActorClient,
useCurrentUserIdentities,
} from "@/composition/apollo/actor";
import { useMutation, useQuery, useApolloClient } from "@vue/apollo-composable";
import { useAvatarMaxSize } from "@/composition/config";
import { computed, inject, reactive, ref, watch } from "vue";
@@ -233,6 +241,8 @@ const props = defineProps<{ isUpdate: boolean; identityName?: string }>();
const { currentActor } = useCurrentActorClient();
const { identities } = useCurrentUserIdentities();
const {
result: personResult,
onError: onPersonError,
@@ -459,13 +469,22 @@ const {
},
}));
createIdentityDone(() => {
createIdentityDone(async () => {
notifier?.success(
t("Identity {displayName} created", {
displayName: displayName(identity.value),
})
);
// If it is the fisrt created identity, then we need to activate this identity
const client = resolveClient();
const data = client.readQuery<{
loggedUser: Pick<ICurrentUser, "actors">;
}>({ query: IDENTITIES });
if (data) {
await maybeUpdateCurrentActorCache(data.loggedUser.actors[0]);
}
router.push({
name: RouteName.UPDATE_IDENTITY,
params: { identityName: identity.value.preferredUsername },
@@ -687,8 +706,11 @@ const redirectIfNoIdentitySelected = async (identityParam?: string) => {
const maybeUpdateCurrentActorCache = async (newIdentity: IPerson) => {
if (currentActor.value) {
// If there is no current actor, update the current actor
if (
currentActor.value.preferredUsername === identity.value.preferredUsername
currentActor.value.preferredUsername ===
identity.value.preferredUsername ||
currentActor.value.id == null
) {
await changeIdentity(newIdentity);
}

View File

@@ -268,11 +268,7 @@ const loginAction = async (e: Event) => {
if (err instanceof NoIdentitiesException && currentUser.value) {
console.debug("No identities, redirecting to profile registration");
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: currentUser.value.email,
userAlreadyActivated: "true",
},
name: RouteName.CREATE_IDENTITY,
});
} else {
console.error(err);

View File

@@ -50,7 +50,7 @@ onUpdateCurrentUserClientDone(async () => {
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
// No need to push to CREATE_IDENTITY, the navbar will do it for us
}
} catch (e) {
console.error(e);

View File

@@ -84,7 +84,7 @@
</i18n-t>
</div>
</div>
<div class="">
<div v-if="!validationSent">
<o-notification variant="warning" v-if="config?.registrationsAllowlist">
{{ t("Registrations are restricted by allowlisting.") }}
</o-notification>
@@ -199,6 +199,29 @@
</div>
</form>
</div>
<div v-else>
<h2 class="title my-5">
{{ t("Your account is nearly ready") }}
</h2>
<i18n-t keypath="A validation email was sent to {email}" tag="p">
<template #email>
<code>{{ credentials.email }}</code>
</template>
</i18n-t>
<p>
{{
t(
"Before you can login, you need to click on the link inside it to validate your account."
)
}}
</p>
<o-button
class="mt-5"
tag="router-link"
:to="{ name: RouteName.HOME }"
>{{ t("Back to homepage") }}</o-button
>
</div>
</section>
</div>
</template>
@@ -212,7 +235,7 @@ 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 { useRoute } from "vue-router";
import { useHead } from "@/utils/head";
import { AbsintheGraphQLErrors } from "@/types/errors.model";
@@ -222,7 +245,7 @@ type credentialsType = { email: string; password: string; locale: string };
const { t, locale } = useI18n({ useScope: "global" });
const route = useRoute();
const router = useRouter();
const validationSent = ref(false);
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
@@ -256,10 +279,7 @@ useHead({
const { onDone, onError, mutate } = useMutation(CREATE_USER);
onDone(() => {
router.push({
name: RouteName.REGISTER_PROFILE,
params: { email: credentials.email },
});
validationSent.value = true;
});
onError((error) => {

View File

@@ -72,8 +72,7 @@ onUpdatingCurrentUserClientDone(async () => {
} 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" },
name: RouteName.CREATE_IDENTITY,
});
}
});