From 681653e0351763eae7397ab172103c65dd48e204 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Tue, 29 Jan 2019 11:02:32 +0100
Subject: [PATCH] Introduce registerPerson mutation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

To register a profile from an unactivated user

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

👤 Fix Person interface use

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

Change host function for data property

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/graphql/actor.ts                       |  28 ++-
 js/src/graphql/user.ts                        |   4 +-
 js/src/router/index.ts                        |  50 +----
 js/src/router/user.ts                         |  59 ++++++
 js/src/types/actor.model.ts                   |   2 +-
 js/src/views/Account/Register.vue             | 147 ++++++---------
 js/src/views/User/Register.vue                | 177 ++++++++++++++++++
 lib/mobilizon/actors/actor.ex                 |  12 ++
 lib/mobilizon/actors/user.ex                  |  17 +-
 lib/mobilizon_web/resolvers/person.ex         |  23 ++-
 lib/mobilizon_web/schema/actors/person.ex     |  20 +-
 lib/mobilizon_web/schema/utils.ex             |   4 +-
 .../resolvers/user_resolver_test.exs          | 148 ++++++++++++++-
 13 files changed, 519 insertions(+), 172 deletions(-)
 create mode 100644 js/src/router/user.ts
 create mode 100644 js/src/views/User/Register.vue

diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts
index 6fa5a65a2..5167e7ff6 100644
--- a/js/src/graphql/actor.ts
+++ b/js/src/graphql/actor.ts
@@ -39,10 +39,34 @@ query {
 
 export const CREATE_PERSON = gql`
 mutation CreatePerson($preferredUsername: String!) {
-  createPerson(preferredUsername: $preferredUsername) {
+  createPerson(
+      preferredUsername: $preferredUsername,
+      name: $name,
+      summary: $summary
+    ) {
     preferredUsername,
     name,
+    summary,
     avatarUrl
   }
 }
-`
\ No newline at end of file
+`;
+
+/**
+ * This one is used only to register the first account. Prefer CREATE_PERSON when creating another identity
+ */
+export const REGISTER_PERSON = gql`
+mutation ($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
+  registerPerson(
+      preferredUsername: $preferredUsername,
+      name: $name,
+      summary: $summary,
+      email: $email
+    ) {
+    preferredUsername,
+    name,
+    summary,
+    avatarUrl,
+  }
+}
+`;
diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts
index 9031370e5..0db4b5486 100644
--- a/js/src/graphql/user.ts
+++ b/js/src/graphql/user.ts
@@ -1,8 +1,8 @@
 import gql from 'graphql-tag';
 
 export const CREATE_USER = gql`
-mutation CreateUser($email: String!, $username: String!, $password: String!) {
-  createUser(email: $email, username: $username, password: $password) {
+mutation CreateUser($email: String!, $password: String!) {
+  createUser(email: $email, password: $password) {
     email,
     confirmationSentAt
   }
diff --git a/js/src/router/index.ts b/js/src/router/index.ts
index 389cffbf5..817b50dd2 100644
--- a/js/src/router/index.ts
+++ b/js/src/router/index.ts
@@ -8,17 +8,12 @@ import Location from '@/views/Location.vue';
 import CreateEvent from '@/views/Event/Create.vue';
 import CategoryList from '@/views/Category/List.vue';
 import CreateCategory from '@/views/Category/Create.vue';
-import Register from '@/views/Account/Register.vue';
-import Login from '@/views/User/Login.vue';
-import Validate from '@/views/User/Validate.vue';
-import ResendConfirmation from '@/views/User/ResendConfirmation.vue';
-import SendPasswordReset from '@/views/User/SendPasswordReset.vue';
-import PasswordReset from '@/views/User/PasswordReset.vue';
 import Profile from '@/views/Account/Profile.vue';
 import CreateGroup from '@/views/Group/Create.vue';
 import Group from '@/views/Group/Group.vue';
 import GroupList from '@/views/Group/GroupList.vue';
 import Identities from '@/views/Account/Identities.vue';
+import userRoutes from './user';
 
 Vue.use(Router);
 
@@ -26,6 +21,7 @@ const router = new Router({
   mode: 'history',
   base: '/',
   routes: [
+    ...userRoutes,
     {
       path: '/',
       name: 'Home',
@@ -69,48 +65,6 @@ const router = new Router({
       component: CreateCategory,
       meta: { requiredAuth: true },
     },
-    {
-      path: '/register',
-      name: 'Register',
-      component: Register,
-      props: true,
-      meta: { requiredAuth: false },
-    },
-    {
-      path: '/resend-instructions',
-      name: 'ResendConfirmation',
-      component: ResendConfirmation,
-      props: true,
-      meta: { requiresAuth: false },
-    },
-    {
-      path: '/password-reset/send',
-      name: 'SendPasswordReset',
-      component: SendPasswordReset,
-      props: true,
-      meta: { requiresAuth: false },
-    },
-    {
-      path: '/password-reset/:token',
-      name: 'PasswordReset',
-      component: PasswordReset,
-      meta: { requiresAuth: false },
-      props: true,
-    },
-    {
-      path: '/validate/:token',
-      name: 'Validate',
-      component: Validate,
-      props: true,
-      meta: { requiresAuth: false },
-    },
-    {
-      path: '/login',
-      name: 'Login',
-      component: Login,
-      props: true,
-      meta: { requiredAuth: false },
-    },
     {
       path: '/identities',
       name: 'Identities',
diff --git a/js/src/router/user.ts b/js/src/router/user.ts
new file mode 100644
index 000000000..bdec67529
--- /dev/null
+++ b/js/src/router/user.ts
@@ -0,0 +1,59 @@
+import RegisterUser from '@/views/User/Register.vue';
+import RegisterProfile from '@/views/Account/Register.vue';
+import Login from '@/views/User/Login.vue';
+import Validate from '@/views/User/Validate.vue';
+import ResendConfirmation from '@/views/User/ResendConfirmation.vue';
+import SendPasswordReset from '@/views/User/SendPasswordReset.vue';
+import PasswordReset from '@/views/User/PasswordReset.vue';
+
+export default [
+    {
+        path: '/register/user',
+        name: 'Register',
+        component: RegisterUser,
+        props: true,
+        meta: { requiredAuth: false },
+    },
+    {
+        path: '/register/profile',
+        name: 'RegisterProfile',
+        component: RegisterProfile,
+        props: true,
+        meta: { requiredAuth: false },
+    },
+    {
+        path: '/resend-instructions',
+        name: 'ResendConfirmation',
+        component: ResendConfirmation,
+        props: true,
+        meta: { requiresAuth: false },
+    },
+    {
+        path: '/password-reset/send',
+        name: 'SendPasswordReset',
+        component: SendPasswordReset,
+        props: true,
+        meta: { requiresAuth: false },
+    },
+    {
+        path: '/password-reset/:token',
+        name: 'PasswordReset',
+        component: PasswordReset,
+        meta: { requiresAuth: false },
+        props: true,
+    },
+    {
+        path: '/validate/:token',
+        name: 'Validate',
+        component: Validate,
+        props: true,
+        meta: { requiresAuth: false },
+    },
+    {
+        path: '/login',
+        name: 'Login',
+        component: Login,
+        props: true,
+        meta: { requiredAuth: false },
+    },
+];
\ No newline at end of file
diff --git a/js/src/types/actor.model.ts b/js/src/types/actor.model.ts
index 78891e493..a5803c0b2 100644
--- a/js/src/types/actor.model.ts
+++ b/js/src/types/actor.model.ts
@@ -2,7 +2,7 @@ export interface IActor {
     id: string;
     url: string;
     name: string;
-    domain: string;
+    domain: string|null;
     summary: string;
     preferredUsername: string;
     suspended: boolean;
diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue
index 543c23ff6..a644049f0 100644
--- a/js/src/views/Account/Register.vue
+++ b/js/src/views/Account/Register.vue
@@ -10,102 +10,63 @@
     <section>
       <div class="container">
         <div class="columns is-mobile">
-          <div class="column">
-            <div class="content">
-              <h2 class="subtitle" v-translate>Features</h2>
-              <ul>
-                <li v-translate>Create your communities and your events</li>
-                <li v-translate>Other stuff…</li>
-              </ul>
-            </div>
-            <p v-translate>
-              Learn more on
-              <a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
-            </p>
-            <hr>
-            <div class="content">
-              <h2 class="subtitle" v-translate>About this instance</h2>
-              <p>
-                <translate>Your local administrator resumed it's policy:</translate>
-              </p>
-              <ul>
-                <li v-translate>Please be nice to each other</li>
-                <li v-translate>meditate a bit</li>
-              </ul>
-              <p>
-                <translate>Please read the full rules</translate>
-              </p>
-            </div>
-          </div>
           <div class="column">
             <form v-if="!validationSent">
               <div class="columns is-mobile is-centered">
                 <div class="column is-narrow">
                   <figure class="image is-64x64">
                     <transition name="avatar">
-                      <v-gravatar v-bind="{email: credentials.email}" default-img="mp"></v-gravatar>
+                      <v-gravatar v-bind="{email: email}" default-img="mp"></v-gravatar>
                     </transition>
                   </figure>
                 </div>
               </div>
 
-              <b-field label="Email">
-                <b-input
-                  aria-required="true"
-                  required
-                  type="email"
-                  v-model="credentials.email"
-                  @blur="showGravatar = true"
-                  @focus="showGravatar = false"
-                />
+              <b-field
+                :label="$gettext('Username')"
+                :type="errors.preferred_username ? 'is-danger' : null"
+                :message="errors.preferred_username"
+              >
+                <b-field>
+                  <b-input
+                    aria-required="true"
+                    required
+                    expanded
+                    v-model="person.preferredUsername"
+                  />
+                  <p class="control">
+                    <span class="button is-static">@{{ host }}</span>
+                  </p>
+                </b-field>
               </b-field>
 
-              <b-field label="Username">
-                <b-input aria-required="true" required v-model="credentials.username"/>
+              <b-field :label="$gettext('Displayed name')">
+                <b-input v-model="person.name"/>
               </b-field>
 
-              <b-field label="Password">
-                <b-input
-                  aria-required="true"
-                  required
-                  type="password"
-                  password-reveal
-                  minlength="6"
-                  v-model="credentials.password"
-                />
+              <b-field :label="$gettext('Description')">
+                <b-input type="textarea" v-model="person.summary"/>
               </b-field>
 
               <b-field grouped>
                 <div class="control">
                   <button type="button" class="button is-primary" @click="submit()">
-                    <translate>Register</translate>
+                    <translate>Create my profile</translate>
                   </button>
                 </div>
-                <div class="control">
-                  <router-link
-                    class="button is-text"
-                    :to="{ name: 'ResendConfirmation', params: { email: credentials.email }}"
-                  >
-                    <translate>Didn't receive the instructions ?</translate>
-                  </router-link>
-                </div>
-                <div class="control">
-                  <router-link
-                    class="button is-text"
-                    :to="{ name: 'Login', params: { email: credentials.email, password: credentials.password }}"
-                    :disabled="validationSent"
-                  >
-                    <translate>Login</translate>
-                  </router-link>
-                </div>
               </b-field>
             </form>
 
             <div v-if="validationSent">
               <b-message title="Success" type="is-success">
-                <h2>
-                  <translate>A validation email was sent to %{email}</translate>
+                <h2 class="title">
+                  <translate
+                    :translate-params="{ username: person.preferredUsername }"
+                  >Your account is nearly ready, %{username}</translate>
                 </h2>
+                <p>
+                  <translate>A validation email was sent to %{email}</translate>
+                </p>
                 <p>
                   <translate>Before you can login, you need to click on the link inside it to validate your account</translate>
                 </p>
@@ -120,8 +81,9 @@
 
 <script lang="ts">
 import Gravatar from "vue-gravatar";
-import { CREATE_USER } from "@/graphql/user";
 import { Component, Prop, Vue } from "vue-property-decorator";
+import { IPerson } from "@/types/actor.model";
+import { REGISTER_PERSON } from "@/graphql/actor";
 import { MOBILIZON_INSTANCE_HOST } from "@/api/_entrypoint";
 
 @Component({
@@ -130,37 +92,42 @@ import { MOBILIZON_INSTANCE_HOST } from "@/api/_entrypoint";
   }
 })
 export default class Register extends Vue {
-  @Prop({ type: String, required: false, default: "" }) email!: string;
-  @Prop({ type: String, required: false, default: "" }) password!: string;
+  @Prop({ type: String, required: true })
+  email!: string;
+  host: string = MOBILIZON_INSTANCE_HOST;
 
-  credentials = {
-    username: "",
-    email: this.email,
-    password: this.password
-  } as { username: string; email: string; password: string };
-  errors: string[] = [];
+  person: IPerson = {
+    preferredUsername: "",
+    name: "",
+    summary: "",
+    id: "",
+    url: "",
+    suspended: false,
+    avatarUrl: "", // TODO : Use Gravatar here
+    bannerUrl: "",
+    domain: null,
+  };
+  errors: object = {};
   validationSent: boolean = false;
+  sendingValidation: boolean = false;
   showGravatar: boolean = false;
 
-  host() {
-    return MOBILIZON_INSTANCE_HOST;
-  }
-
-  validEmail() {
-    return this.credentials.email.includes("@") === true
-      ? "v-gravatar"
-      : "avatar";
-  }
-
   async submit() {
     try {
-      this.validationSent = true;
+      this.sendingValidation = true;
+      this.errors = {};
       await this.$apollo.mutate({
-        mutation: CREATE_USER,
-        variables: this.credentials
+        mutation: REGISTER_PERSON,
+        variables: Object.assign({ email: this.email }, this.person)
       });
+      this.validationSent = true;
     } catch (error) {
+      this.errors = error.graphQLErrors.reduce((acc, error) => {
+        acc[error.details] = error.message;
+        return acc;
+      }, {});
       console.error(error);
+      console.error(this.errors);
     }
   }
 }
diff --git a/js/src/views/User/Register.vue b/js/src/views/User/Register.vue
new file mode 100644
index 000000000..ed4a654c1
--- /dev/null
+++ b/js/src/views/User/Register.vue
@@ -0,0 +1,177 @@
+<template>
+  <div>
+    <section class="hero">
+      <div class="hero-body">
+        <h1 class="title">
+          <translate>Register an account on Mobilizon!</translate>
+        </h1>
+      </div>
+    </section>
+    <section>
+      <div class="container">
+        <div class="columns is-mobile">
+          <div class="column">
+            <div class="content">
+              <h2 class="subtitle" v-translate>Features</h2>
+              <ul>
+                <li v-translate>Create your communities and your events</li>
+                <li v-translate>Other stuff…</li>
+              </ul>
+            </div>
+            <p v-translate>
+              Learn more on
+              <a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
+            </p>
+            <hr>
+            <div class="content">
+              <h2 class="subtitle" v-translate>About this instance</h2>
+              <p>
+                <translate>Your local administrator resumed it's policy:</translate>
+              </p>
+              <ul>
+                <li v-translate>Please be nice to each other</li>
+                <li v-translate>meditate a bit</li>
+              </ul>
+              <p>
+                <translate>Please read the full rules</translate>
+              </p>
+            </div>
+          </div>
+          <div class="column">
+            <form @submit="submit">
+              <b-field
+                label="Email"
+                :type="errors.email ? 'is-danger' : null"
+                :message="errors.email"
+              >
+                <b-input
+                  aria-required="true"
+                  required
+                  type="email"
+                  v-model="credentials.email"
+                  @blur="showGravatar = true"
+                  @focus="showGravatar = false"
+                />
+              </b-field>
+
+              <b-field
+                label="Password"
+                :type="errors.password ? 'is-danger' : null"
+                :message="errors.password"
+              >
+                <b-input
+                  aria-required="true"
+                  required
+                  type="password"
+                  password-reveal
+                  minlength="6"
+                  v-model="credentials.password"
+                />
+              </b-field>
+
+              <b-field grouped>
+                <div class="control">
+                  <button type="button" class="button is-primary" @click="submit()">
+                    <translate>Register</translate>
+                  </button>
+                </div>
+                <div class="control">
+                  <router-link
+                    class="button is-text"
+                    :to="{ name: 'ResendConfirmation', params: { email: credentials.email }}"
+                  >
+                    <translate>Didn't receive the instructions ?</translate>
+                  </router-link>
+                </div>
+                <div class="control">
+                  <router-link
+                    class="button is-text"
+                    :to="{ name: 'Login', params: { email: credentials.email, password: credentials.password }}"
+                    :disabled="sendingValidation"
+                  >
+                    <translate>Login</translate>
+                  </router-link>
+                </div>
+              </b-field>
+            </form>
+
+            <div v-if="errors.length > 0">
+              <b-message type="is-danger" v-for="error in errors" :key="error">
+                <translate>{{ error }}</translate>
+              </b-message>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script lang="ts">
+import Gravatar from "vue-gravatar";
+import { CREATE_USER } from "@/graphql/user";
+import { Component, Prop, Vue } from "vue-property-decorator";
+
+@Component({
+  components: {
+    "v-gravatar": Gravatar
+  }
+})
+export default class Register extends Vue {
+  @Prop({ type: String, required: false, default: "" }) email!: string;
+  @Prop({ type: String, required: false, default: "" }) password!: string;
+
+  credentials = {
+    email: this.email,
+    password: this.password
+  } as { email: string; password: string };
+  errors: object = {};
+  sendingValidation: boolean = false;
+  validationSent: boolean = false;
+  showGravatar: boolean = false;
+
+  validEmail() {
+    return this.credentials.email.includes("@") === true
+      ? "v-gravatar"
+      : "avatar";
+  }
+
+  async submit() {
+    try {
+      this.sendingValidation = true;
+      this.errors = {};
+      await this.$apollo.mutate({
+        mutation: CREATE_USER,
+        variables: this.credentials
+      });
+      this.validationSent = true;
+      this.$router.push({
+        name: "RegisterProfile",
+        params: { email: this.credentials.email }
+      });
+    } catch (error) {
+      console.error(error);
+      this.errors = error.graphQLErrors.reduce((acc, error) => {
+        acc[error.details] = error.message;
+        return acc;
+      }, {});
+      console.log(this.errors);
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.avatar-enter-active {
+  transition: opacity 1s ease;
+}
+
+.avatar-enter,
+.avatar-leave-to {
+  opacity: 0;
+}
+
+.avatar-leave {
+  display: none;
+}
+</style>
diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex
index 8dacdfa5c..b264eee4a 100644
--- a/lib/mobilizon/actors/actor.ex
+++ b/lib/mobilizon/actors/actor.ex
@@ -103,6 +103,8 @@ defmodule Mobilizon.Actors.Actor do
       :user_id
     ])
     |> build_urls()
+    # Needed because following constraint can't work for domain null values (local)
+    |> unique_username_validator()
     |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
     |> unique_constraint(:url, name: :actors_url_index)
     |> validate_required([:preferred_username, :keys, :suspended, :url, :type])
@@ -177,6 +179,16 @@ defmodule Mobilizon.Actors.Actor do
     |> put_change(:local, true)
   end
 
+  def unique_username_validator(
+        %Ecto.Changeset{changes: %{preferred_username: username}} = changeset
+      ) do
+    if Actors.get_local_actor_by_name(username) do
+      changeset |> add_error(:preferred_username, "Username is already taken")
+    else
+      changeset
+    end
+  end
+
   @spec build_urls(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
   defp build_urls(changeset, type \\ :Person)
 
diff --git a/lib/mobilizon/actors/user.ex b/lib/mobilizon/actors/user.ex
index 9fc1c6792..d69be537b 100644
--- a/lib/mobilizon/actors/user.ex
+++ b/lib/mobilizon/actors/user.ex
@@ -30,6 +30,7 @@ defmodule Mobilizon.Actors.User do
       |> cast(attrs, [
         :email,
         :role,
+        :password,
         :password_hash,
         :confirmed_at,
         :confirmation_sent_at,
@@ -38,13 +39,13 @@ defmodule Mobilizon.Actors.User do
         :reset_password_token
       ])
       |> validate_required([:email])
-      |> unique_constraint(:email, message: "registration.error.email_already_used")
-      |> validate_format(:email, ~r/@/)
+      |> unique_constraint(:email, message: "This email is already used.")
+      |> validate_email()
       |> validate_length(
         :password,
         min: 6,
         max: 100,
-        message: "registration.error.password_too_short"
+        message: "The choosen password is too short."
       )
 
     if Map.has_key?(attrs, :default_actor) do
@@ -57,21 +58,13 @@ defmodule Mobilizon.Actors.User do
   def registration_changeset(struct, params) do
     struct
     |> changeset(params)
-    |> cast(params, ~w(password)a, [])
     |> cast_assoc(:default_actor)
     |> validate_required([:email, :password])
-    |> validate_email()
-    |> validate_length(
-      :password,
-      min: 6,
-      max: 100,
-      message: "registration.error.password_too_short"
-    )
     |> hash_password()
     |> save_confirmation_token()
     |> unique_constraint(
       :confirmation_token,
-      message: "regisration.error.confirmation_token_already_in_use"
+      message: "The registration is already in use, this looks like an issue on our side."
     )
   end
 
diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex
index 347a70a7e..2258d49d7 100644
--- a/lib/mobilizon_web/resolvers/person.ex
+++ b/lib/mobilizon_web/resolvers/person.ex
@@ -3,7 +3,7 @@ defmodule MobilizonWeb.Resolvers.Person do
   Handles the person-related GraphQL calls
   """
   alias Mobilizon.Actors
-  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Actors.{Actor, User}
   alias Mobilizon.Service.ActivityPub
 
   @deprecated "Use find_person/3 or find_group/3 instead"
@@ -52,6 +52,9 @@ defmodule MobilizonWeb.Resolvers.Person do
     {:error, "You need to be logged-in to view your list of identities"}
   end
 
+  @doc """
+  This function is used to create more identities from an existing user
+  """
   def create_person(_parent, %{preferred_username: _preferred_username} = args, %{
         context: %{current_user: user}
       }) do
@@ -59,9 +62,23 @@ defmodule MobilizonWeb.Resolvers.Person do
 
     with {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
       {:ok, new_person}
+    end
+  end
+
+  @doc """
+  This function is used to register a person afterwards the user has been created (but not activated)
+  """
+  def register_person(_parent, args, _resolution) do
+    with {:ok, %User{} = user} <- Actors.get_user_by_email(args.email),
+         {:no_actor, nil} <- {:no_actor, Actors.get_actor_for_user(user)},
+         args <- Map.put(args, :user_id, user.id),
+         {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
+      {:ok, new_person}
     else
-      {:error, %Ecto.Changeset{} = _e} ->
-        {:error, "Unable to create a profile with this username"}
+      {:error, :user_not_found} ->
+        {:error, "User with email not found"}
+      {:no_actor, _} ->
+        {:error, "You already have a profile for this user"}
     end
   end
 end
diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex
index 70610a324..4397caf6e 100644
--- a/lib/mobilizon_web/schema/actors/person.ex
+++ b/lib/mobilizon_web/schema/actors/person.ex
@@ -6,6 +6,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
   import Absinthe.Resolution.Helpers, only: [dataloader: 1]
   alias Mobilizon.Events
   alias MobilizonWeb.Resolvers
+  import MobilizonWeb.Schema.Utils
 
   @desc """
   Represents a person identity
@@ -69,11 +70,24 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
     @desc "Create a new person for user"
     field :create_person, :person do
       arg(:preferred_username, non_null(:string))
-      arg(:name, :string, description: "The displayed name for the new profile")
 
-      arg(:description, :string, description: "The summary for the new profile", default_value: "")
+      arg(:name, :string, description: "The displayed name for the new profile", default_value: "")
 
-      resolve(&Resolvers.Person.create_person/3)
+      arg(:summary, :string, description: "The summary for the new profile", default_value: "")
+
+      resolve(handle_errors(&Resolvers.Person.create_person/3))
+    end
+
+    @desc "Register a first profile on registration"
+    field :register_person, :person do
+      arg(:preferred_username, non_null(:string))
+
+      arg(:name, :string, description: "The displayed name for the new profile", default_value: "")
+
+      arg(:summary, :string, description: "The summary for the new profile", default_value: "")
+      arg(:email, non_null(:string), description: "The email from the user previously created")
+
+      resolve(handle_errors(&Resolvers.Person.register_person/3))
     end
   end
 end
diff --git a/lib/mobilizon_web/schema/utils.ex b/lib/mobilizon_web/schema/utils.ex
index a2bf553df..f7371ff8f 100644
--- a/lib/mobilizon_web/schema/utils.ex
+++ b/lib/mobilizon_web/schema/utils.ex
@@ -12,8 +12,8 @@ defmodule MobilizonWeb.Schema.Utils do
     # {:error, [email: {"has already been taken", []}]}
     errors =
       changeset.errors
-      |> Enum.map(fn {_key, {value, context}} ->
-        [message: "#{value}", details: context]
+      |> Enum.map(fn {key, {value, _context}} ->
+        [message: "#{value}", details: key]
       end)
 
     {:error, errors}
diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs
index de969f41e..9381296bb 100644
--- a/test/mobilizon_web/resolvers/user_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/user_resolver_test.exs
@@ -73,21 +73,25 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
   end
 
   describe "Resolver: Create an user & actor" do
-    @account_creation %{
+    @user_creation %{
       email: "test@demo.tld",
-      password: "long password"
+      password: "long password",
+      username: "toto",
+      name: "Sir Toto",
+      summary: "Sir Toto, prince of the functional tests"
     }
-    @account_creation_bad_email %{
+    @user_creation_bad_email %{
       email: "y@l@",
       password: "long password"
     }
 
-    test "test create_user/3 creates an user", context do
+    test "test create_user/3 creates an user and register_person/3 registers a profile",
+         context do
       mutation = """
           mutation {
             createUser(
-                  email: "#{@account_creation.email}",
-                  password: "#{@account_creation.password}",
+                  email: "#{@user_creation.email}",
+                  password: "#{@user_creation.password}",
               ) {
                 id,
                 email
@@ -99,15 +103,141 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
         context.conn
         |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
 
-      assert json_response(res, 200)["data"]["createUser"]["email"] == @account_creation.email
+      assert json_response(res, 200)["data"]["createUser"]["email"] == @user_creation.email
+
+      mutation = """
+          mutation {
+            registerPerson(
+              preferredUsername: "#{@user_creation.username}",
+              name: "#{@user_creation.name}",
+              summary: "#{@user_creation.summary}",
+              email: "#{@user_creation.email}",
+              ) {
+                preferredUsername,
+                name,
+                summary,
+                avatarUrl,
+              }
+            }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["data"]["registerPerson"]["preferredUsername"] ==
+               @user_creation.username
+    end
+
+    test "register_person/3 doesn't register a profile from an unknown email", context do
+      mutation = """
+          mutation {
+            createUser(
+                  email: "#{@user_creation.email}",
+                  password: "#{@user_creation.password}",
+              ) {
+                id,
+                email
+              }
+            }
+      """
+
+      context.conn
+      |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      mutation = """
+          mutation {
+            registerPerson(
+              preferredUsername: "#{@user_creation.username}",
+              name: "#{@user_creation.name}",
+              summary: "#{@user_creation.summary}",
+              email: "random",
+              ) {
+                preferredUsername,
+                name,
+                summary,
+                avatarUrl,
+              }
+            }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] ==
+               "User with email not found"
+    end
+
+    test "register_person/3 can't be called with an existing profile", context do
+      mutation = """
+          mutation {
+            createUser(
+                  email: "#{@user_creation.email}",
+                  password: "#{@user_creation.password}",
+              ) {
+                id,
+                email
+              }
+            }
+      """
+
+      context.conn
+      |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      mutation = """
+          mutation {
+            registerPerson(
+              preferredUsername: "#{@user_creation.username}",
+              name: "#{@user_creation.name}",
+              summary: "#{@user_creation.summary}",
+              email: "#{@user_creation.email}",
+              ) {
+                preferredUsername,
+                name,
+                summary,
+                avatarUrl,
+              }
+            }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["data"]["registerPerson"]["preferredUsername"] ==
+               @user_creation.username
+
+      mutation = """
+          mutation {
+            registerPerson(
+              preferredUsername: "#{@user_creation.username}",
+              name: "#{@user_creation.name}",
+              summary: "#{@user_creation.summary}",
+              email: "#{@user_creation.email}",
+              ) {
+                preferredUsername,
+                name,
+                summary,
+                avatarUrl,
+              }
+            }
+      """
+
+      res =
+        context.conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] ==
+               "You already have a profile for this user"
     end
 
     test "test create_user/3 doesn't create an user with bad email", context do
       mutation = """
           mutation {
             createUser(
-                  email: "#{@account_creation_bad_email.email}",
-                  password: "#{@account_creation.password}",
+                  email: "#{@user_creation_bad_email.email}",
+                  password: "#{@user_creation_bad_email.password}",
               ) {
                 id,
                 email