From 69048089a50e909a555ee08bae5e91a77d0cafc6 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 17 Dec 2019 12:09:24 +0100 Subject: [PATCH] Add a config option to whitelist users registration Through whole email or domain email Signed-off-by: Thomas Citharel --- config/config.exs | 1 + js/src/graphql/config.ts | 1 + js/src/i18n/de.json | 1 - js/src/i18n/en_US.json | 103 +++---- js/src/i18n/fr_FR.json | 3 +- js/src/i18n/nl.json | 1 - js/src/i18n/oc.json | 1 - js/src/i18n/pl.json | 1 - js/src/i18n/sv.json | 1 - js/src/router/guards/register-guard.ts | 2 +- js/src/types/config.model.ts | 1 + js/src/utils/errors.ts | 5 + js/src/views/User/Register.vue | 8 +- lib/mobilizon/config.ex | 6 + lib/mobilizon_web/resolvers/config.ex | 1 + lib/mobilizon_web/resolvers/user.ex | 32 ++- lib/mobilizon_web/schema/config.ex | 1 + schema.graphql | 251 +++++++++--------- .../resolvers/user_resolver_test.exs | 143 ++++++++-- 19 files changed, 351 insertions(+), 212 deletions(-) diff --git a/config/config.exs b/config/config.exs index 2780bfc92..7a2472555 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,6 +18,7 @@ config :mobilizon, :instance, version: "1.0.0-dev", hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost", registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false, + registration_email_whitelist: [], demo: System.get_env("MOBILIZON_INSTANCE_DEMO_MODE") || false, repository: Mix.Project.config()[:source_url], allow_relay: true, diff --git a/js/src/graphql/config.ts b/js/src/graphql/config.ts index 8b424ea32..a128eba38 100644 --- a/js/src/graphql/config.ts +++ b/js/src/graphql/config.ts @@ -6,6 +6,7 @@ query { name, description, registrationsOpen, + registrationsWhitelist, demoMode, countryCode, location { diff --git a/js/src/i18n/de.json b/js/src/i18n/de.json index 075eed19a..c96474cd8 100644 --- a/js/src/i18n/de.json +++ b/js/src/i18n/de.json @@ -12,7 +12,6 @@ "Add to my calendar": "Zu meinem Kalender hinzufügen", "Additional comments": "Zusätzliche Kommentare", "Administration": "Administration", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "Alle Daten werden alle 48 Stunden gelöscht, also benutze diesen Service bitte nur zu Testzwecken.", "All the places have already been taken": "Alle Plätze sind besetzt|Ein Platz ist noch verfügbar|{places} Plätze sind noch verfügbar", "Allow all comments": "Erlaube alle Kommentare", "An error has occurred.": "Ein Fehler ist aufgetreten.", diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 5894fa2ff..c8c14d7eb 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -7,17 +7,21 @@ "About this event": "About this event", "About this instance": "About this instance", "About": "About", + "Accepted": "Accepted", + "Add a note": "Add a note", "Add an address": "Add an address", + "Add an instance": "Add an instance", "Add some tags": "Add some tags", "Add to my calendar": "Add to my calendar", "Add": "Add", "Additional comments": "Additional comments", "Administration": "Administration", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "All data will be deleted every 48 hours, so please don't use this for anything real.", "All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available", "Allow all comments": "Allow all comments", "An error has occurred.": "An error has occurred.", "Approve": "Approve", + "Are you sure you want to delete this comment? This action cannot be undone.": "Are you sure you want to delete this comment? This action cannot be undone.", + "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Are you sure you want to cancel the event creation? You'll lose all modifications.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Are you sure you want to cancel the event edition? You'll lose all modifications.", "Are you sure you want to cancel your participation at event \"{title}\"?": "Are you sure you want to cancel your participation at event \"{title}\"?", @@ -40,6 +44,9 @@ "Click to select": "Click to select", "Click to upload": "Click to upload", "Close comments for all (except for admins)": "Close comments for all (except for admins)", + "Close": "Close", + "Closed": "Closed", + "Comment deleted": "Comment deleted", "Comment from @{username} reported": "Comment from @{username} reported", "Comments have been closed.": "Comments have been closed.", "Comments": "Comments", @@ -60,18 +67,25 @@ "Create": "Create", "Creator": "Creator", "Current identity has been changed to {identityName} in order to manage this event.": "Current identity has been changed to {identityName} in order to manage this event.", + "Dashboard": "Dashboard", "Date and time settings": "Date and time settings", "Date parameters": "Date parameters", + "Date": "Date", + "Delete Comment": "Delete Comment", + "Delete Event": "Delete Event", "Delete event": "Delete event", "Delete this identity": "Delete this identity", "Delete your identity": "Delete your identity", "Delete {eventTitle}": "Delete {eventTitle}", "Delete {preferredUsername}": "Delete {preferredUsername}", "Delete": "Delete", + "Deleting comment": "Deleting comment", + "Deleting event": "Deleting event", "Description": "Description", "Didn't receive the instructions ?": "Didn't receive the instructions ?", "Display name": "Display name", "Display participation price": "Display participation price", + "Domain": "Domain", "Draft": "Draft", "Drafts": "Drafts", "Edit": "Edit", @@ -96,12 +110,15 @@ "Event {eventTitle} reported": "Event {eventTitle} reported", "Event": "Event", "Events": "Events", + "Ex: test.mobilizon.org": "Ex: test.mobilizon.org", "Exclude": "Exclude", "Explore": "Explore", "Featured events": "Featured events", "Features": "Features", "Find an address": "Find an address", "Find an instance": "Find an instance", + "Followers": "Followers", + "Followings": "Followings", "For instance: London, Taekwondo, Architecture…": "For instance: London, Taekwondo, Architecture…", "Forgot your password ?": "Forgot your password ?", "From a birthday party with friends and family to a march for climate change, right now, our gatherings are trapped inside the tech giants’ platforms. How can we organize, how can we click “Attend,” without providing private data to Facebook or locking ourselves up inside MeetUp?": "From a birthday party with friends and family to a march for climate change, right now, our gatherings are trapped inside the tech giants’ platforms. How can we organize, how can we click “Attend,” without providing private data to Facebook or locking ourselves up inside MeetUp?", @@ -130,6 +147,7 @@ "Impossible to login, your email or password seems incorrect.": "Impossible to login, your email or password seems incorrect.", "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.": "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.", "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating their own event platform.": "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating their own event platform.", + "Instances": "Instances", "Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance", "Last published event": "Last published event", "Last week": "Last week", @@ -147,6 +165,7 @@ "Login on Mobilizon!": "Login on Mobilizon!", "Login": "Login", "Manage participations": "Manage participations", + "Mark as resolved": "Mark as resolved", "Members": "Members", "Mobilizon is a free/libre software that will allow communities to create their own spaces to publish events in order to better emancipate themselves from tech giants.": "Mobilizon is a free/libre software that will allow communities to create their own spaces to publish events in order to better emancipate themselves from tech giants.", "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020.": "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020.", @@ -156,16 +175,26 @@ "My events": "My events", "My identities": "My identities", "Name": "Name", + "New note": "New note", "New password": "New password", "No actors found": "No actors found", "No address defined": "No address defined", + "No closed reports yet": "No closed reports yet", + "No comment": "No comment", "No comments yet": "No comments yet", "No end date": "No end date", "No events found": "No events found", "No group found": "No group found", "No groups found": "No groups found", + "No instance follows your instance yet.": "No instance follows your instance yet.", + "No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances", + "No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances", + "No instance to remove|Remove instance|Remove {number} instances": "No instances to remove|Remove instance|Remove {number} instances", + "No open reports yet": "No open reports yet", + "No resolved reports yet": "No resolved reports yet", "No results for \"{queryText}\"": "No results for \"{queryText}\"", "No user account with this email was found. Maybe you made a typo?": "No user account with this email was found. Maybe you made a typo?", + "Notes": "Notes", "Number of places": "Number of places", "OK": "OK", "Old password": "Old password", @@ -176,6 +205,7 @@ "One person is going": "No one is going | One person is going | {approved} persons are going", "Only accessible through link and search (private)": "Only accessible through link and search (private)", "Only alphanumeric characters and underscores are supported.": "Only alphanumeric characters and underscores are supported.", + "Open": "Open", "Opened reports": "Opened reports", "Organized by {name}": "Organized by {name}", "Organized": "Organized", @@ -194,6 +224,7 @@ "Password reset": "Password reset", "Password": "Password", "Past events": "Passed events", + "Pending": "Pending", "Pick an identity": "Pick an identity", "Please check your spam folder if you didn't receive the email.": "Please check your spam folder if you didn't receive the email.", "Please contact this instance's Mobilizon admin if you think this is a mistake.": "Please contact this instance's Mobilizon admin if you think this is a mistake.", @@ -219,15 +250,25 @@ "Register for an event by choosing one of your identities": "Register for an event by choosing one of your identities", "Register": "Register", "Registration is currently closed.": "Registration is currently closed.", + "Registrations are restricted by whitelisting.": "Registrations are restricted by whitelisting.", "Reject": "Reject", "Rejected participations": "Rejected participations", "Rejected": "Rejected", + "Reopen": "Reopen", + "Reply": "Reply", "Report this comment": "Report this comment", "Report this event": "Report this event", "Report": "Report", + "Reported by someone on {domain}": "Reported by someone on {domain}", + "Reported by {reporter}": "Reported by {reporter}", + "Reported by": "Reported by", + "Reported identity": "Reported identity", + "Reported": "Reported", + "Reports": "Reports", "Requests": "Requests", "Resend confirmation email": "Resend confirmation email", "Reset my password": "Reset my password", + "Resolved": "Resolved", "Save draft": "Save draft", "Save": "Save", "Search events, groups, etc.": "Search events, groups, etc.", @@ -273,13 +314,17 @@ "To confirm, type your event title \"{eventTitle}\"": "To confirm, type your event title \"{eventTitle}\"", "To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"", "Transfer to {outsideDomain}": "Transfer to {outsideDomain}", + "Type": "Type", "Unfortunately, this instance isn't opened to registrations": "Unfortunately, this instance isn't opened to registrations", "Unfortunately, your participation request was rejected by the organizers.": "Unfortunately, your participation request was rejected by the organizers.", + "Unknown actor": "Unknown actor", "Unknown error.": "Unknown error.", + "Unknown": "Unknown", "Unsaved changes": "Unsaved changes", "Upcoming": "Upcoming", "Update event {name}": "Update event {name}", "Update my event": "Update my event", + "Updated": "Updated", "Username": "Username", "Users": "Users", "View a reply": "|View one reply|View {totalReplies} replies", @@ -307,6 +352,7 @@ "You can add tags by hitting the Enter key or by adding a comma": "You can add tags by hitting the Enter key or by adding a comma", "You can try another search term or drag and drop the marker on the map": "You can try another search term or drag and drop the marker on the map", "You can't remove your last identity.": "You can't remove your last identity.", + "You don't follow any instances yet.": "You don't follow any instances yet.", "You have been disconnected": "You have been disconnected", "You have cancelled your participation": "You have cancelled your participation", "You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days", @@ -317,12 +363,16 @@ "Your account has been validated": "Your account has been validated", "Your account is being validated": "Your account is being validated", "Your account is nearly ready, {username}": "Your account is nearly ready, {username}", + "Your email is not whitelisted, you can't register.": "Your email is not whitelisted, you can't register.", "Your local administrator resumed its policy:": "Your local administrator resumed its policy:", "Your participation has been confirmed": "Your participation has been confirmed", + "Your participation has been rejected": "Your participation has been rejected", "Your participation has been requested": "Your participation has been requested", + "Your participation status has been changed": "Your participation status has been changed", "[This comment has been deleted]": "[This comment has been deleted]", "[deleted]": "[deleted]", "a decentralised federation protocol": "a decentralised federation protocol", + "as {identity}": "as {identity}", "e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot", "firstDayOfWeek": "0", "iCal Feed": "iCal Feed", @@ -332,59 +382,10 @@ "resend confirmation email": "resend confirmation email", "respect of the fundamental freedoms": "respect of the fundamental freedoms", "with another identity…": "with another identity…", - "as {identity}": "as {identity}", "{approved} / {total} seats": "{approved} / {total} seats", "{count} participants": "No participants yet | One participant | {count} participants", "{count} requests waiting": "{count} requests waiting", "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.", "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks", - "© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors", - "Reply": "Reply", - "Accepted": "Accepted", - "Pending": "Pending", - "No instance to remove|Remove instance|Remove {number} instances": "No instances to remove|Remove instance|Remove {number} instances", - "Dashboard": "Dashboard", - "Reports": "Reports", - "Mark as resolved": "Mark as resolved", - "Reopen": "Reopen", - "Close": "Close", - "Reported identity": "Reported identity", - "Reported by": "Reported by", - "Reported": "Reported", - "Updated": "Updated", - "Open": "Open", - "Closed": "Closed", - "Resolved": "Resolved", - "Unknown": "Unknown", - "No comment": "No comment", - "Notes": "Notes", - "New note": "New note", - "Add a note": "Add a note", - "Deleting event": "Deleting event", - "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.", - "Delete Event": "Delete Event", - "Type": "Type", - "Domain": "Domain", - "Date": "Date", - "No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances", - "No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances", - "No instance follows your instance yet.": "No instance follows your instance yet.", - "Followers": "Followers", - "Add an instance": "Add an instance", - "Ex: test.mobilizon.org": "Ex: test.mobilizon.org", - "You don't follow any instances yet.": "You don't follow any instances yet.", - "Followings": "Followings", - "Instances": "Instances", - "Reported by {reporter}": "Reported by {reporter}", - "No open reports yet": "No open reports yet", - "No resolved reports yet": "No resolved reports yet", - "No closed reports yet": "No closed reports yet", - "Reported by someone on {domain}": "Reported by someone on {domain}", - "Your participation has been rejected": "Your participation has been rejected", - "Your participation status has been changed": "Your participation status has been changed", - "Unknown actor": "Unknown actor", - "Deleting comment": "Deleting comment", - "Are you sure you want to delete this comment? This action cannot be undone.": "Are you sure you want to delete this comment? This action cannot be undone.", - "Delete Comment": "Delete Comment", - "Comment deleted": "Comment deleted" + "© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors" } \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 6e86fb4f8..264b40d10 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -16,7 +16,6 @@ "Add": "Ajouter", "Additional comments": "Commentaires additionnels", "Administration": "Administration", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "Toutes les données seront effacées toutes les 48 heures, donc n'utilisez pas ce site à des fins autres que de démonstration.", "All the places have already been taken": "Toutes les places ont été prises|Une place est encore disponible|{places} places sont encore disponibles", "Allow all comments": "Autoriser tous les commentaires", "An error has occurred.": "Une erreur est survenue.", @@ -253,6 +252,7 @@ "Register for an event by choosing one of your identities": "S'inscrire à un événement en choisissant une de vos identités", "Register": "S'inscrire", "Registration is currently closed.": "Les inscriptions sont actuellement fermées.", + "Registrations are restricted by whitelisting.": "Les inscriptions sont restreintes par liste blanche.", "Reject": "Rejetter", "Rejected participations": "Participations rejetées", "Rejected": "Rejetés", @@ -365,6 +365,7 @@ "Your account has been validated": "Votre compte a été validé", "Your account is being validated": "Votre compte est en cours de validation", "Your account is nearly ready, {username}": "Votre compte est presque prêt, {username}", + "Your email is not whitelisted, you can't register.": "Votre email n'est pas sur la liste blanche, vous ne pouvez pas vous inscrire.", "Your local administrator resumed its policy:": "Votre administrateur local a résumé sa politique ainsi :", "Your participation has been confirmed": "Votre participation a été confirmée", "Your participation has been rejected": "Votre participation a été rejettée", diff --git a/js/src/i18n/nl.json b/js/src/i18n/nl.json index e172ca004..bb6ffd24f 100644 --- a/js/src/i18n/nl.json +++ b/js/src/i18n/nl.json @@ -12,7 +12,6 @@ "Add to my calendar": "Aan mijn kalender toevoegen", "Additional comments": "Meer opmerkingen", "Administration": "Administratie", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "Alle gegevens worden iedere 48 uur verwijderd, dus gebruik dit enkel als test.", "All the places have already been taken": "Ale plaatsen zijn bezet|Er is nog één plaats vrij|Er zijn nog {places} plaatsen vrij", "Allow all comments": "Alle opmerkingen toestaan", "An error has occurred.": "Er is een fout opgetreden.", diff --git a/js/src/i18n/oc.json b/js/src/i18n/oc.json index 7e35e83f4..c61599f36 100644 --- a/js/src/i18n/oc.json +++ b/js/src/i18n/oc.json @@ -17,7 +17,6 @@ "Add": "Ajustar", "Additional comments": "Comentari adicional", "Administration": "Administracion", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "Estant que totas las donadas son suprimidas cada 48 oras, utilizetz pas aquò per quicòm mai qu’una demostracion.", "All the places have already been taken": "Totas las plaças son presas|Una plaça es encara disponibla|{places} plaças son encara disponiblas", "Allow all comments": "Autorizar totes los comentaris", "An error has occurred.": "Una error s’es producha.", diff --git a/js/src/i18n/pl.json b/js/src/i18n/pl.json index 15e667919..5b5cf9681 100644 --- a/js/src/i18n/pl.json +++ b/js/src/i18n/pl.json @@ -11,7 +11,6 @@ "Add to my calendar": "Dodaj do kalendarza", "Additional comments": "Dodatkowe komentarze", "Administration": "Administracja", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "Wszystkie dane zostaną usunięte co 48 godzin, więc nie używaj tego do rzeczywistych działań.", "Allow all comments": "Pozwól na wszystkie komentarze", "An error has occurred.": "Wystąpił błąd.", "Approve": "Zatwierdź", diff --git a/js/src/i18n/sv.json b/js/src/i18n/sv.json index 9891037c0..a88c81680 100644 --- a/js/src/i18n/sv.json +++ b/js/src/i18n/sv.json @@ -12,7 +12,6 @@ "Add to my calendar": "Lägg till i min kalender", "Additional comments": "Yttligare kommentarer", "Administration": "Administration", - "All data will be deleted every 48 hours, so please don't use this for anything real.": "All data kommer raderas varje 48e timme, så använd inte detta för något riktigt.", "All the places have already been taken": "Alla platser är bokade|Det finns en plats kvar|Det finns {places} kvar", "Allow all comments": "Tillåt alla kommentarer", "An error has occurred.": "Ett fel har uppstått.", diff --git a/js/src/router/guards/register-guard.ts b/js/src/router/guards/register-guard.ts index 5d693ebc6..4a36ec166 100644 --- a/js/src/router/guards/register-guard.ts +++ b/js/src/router/guards/register-guard.ts @@ -12,7 +12,7 @@ export const beforeRegisterGuard: NavigationGuard = async function (to, from, ne const config: IConfig = data.config; - if (!config.registrationsOpen) { + if (!config.registrationsOpen && !config.registrationsWhitelist) { return next({ name: ErrorRouteName.ERROR, query: { code: ErrorCode.REGISTRATION_CLOSED }, diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts index 01afee086..e99a38526 100644 --- a/js/src/types/config.model.ts +++ b/js/src/types/config.model.ts @@ -3,6 +3,7 @@ export interface IConfig { description: string; registrationsOpen: boolean; + registrationsWhitelist: boolean; demoMode: boolean; countryCode: string; location: { diff --git a/js/src/utils/errors.ts b/js/src/utils/errors.ts index 95f8ead37..f7909ce66 100644 --- a/js/src/utils/errors.ts +++ b/js/src/utils/errors.ts @@ -48,6 +48,11 @@ export const errors: IError[] = [ value: i18n.t("The current identity doesn't have any permission on this event. You should probably change it.") as string, suggestRefresh: false, }, + { + match: /Your email is not on the whitelist$/, + value: i18n.t("Your email is not whitelisted, you can't register.") as string, + suggestRefresh: false, + }, { match: /Cannot remove the last identity of a user/, value: i18n.t("You can't remove your last identity.") as string, diff --git a/js/src/views/User/Register.vue b/js/src/views/User/Register.vue index 8b220f2af..9f45e8b5a 100644 --- a/js/src/views/User/Register.vue +++ b/js/src/views/User/Register.vue @@ -29,7 +29,6 @@

@@ -37,6 +36,7 @@
+ {{ $t('Registrations are restricted by whitelisting.') }}
0 + @spec instance_demo_mode? :: boolean def instance_demo_mode?, do: to_boolean(instance_config()[:demo]) diff --git a/lib/mobilizon_web/resolvers/config.ex b/lib/mobilizon_web/resolvers/config.ex index 3c1e40bc7..44ee37f1c 100644 --- a/lib/mobilizon_web/resolvers/config.ex +++ b/lib/mobilizon_web/resolvers/config.ex @@ -30,6 +30,7 @@ defmodule MobilizonWeb.Resolvers.Config do %{ name: Config.instance_name(), registrations_open: Config.instance_registrations_open?(), + registrations_whitelist: Config.instance_registrations_whitelist?(), demo_mode: Config.instance_demo_mode?(), description: Config.instance_description(), location: location, diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex index b6f9bec26..6e5b0c1bd 100644 --- a/lib/mobilizon_web/resolvers/user.ex +++ b/lib/mobilizon_web/resolvers/user.ex @@ -116,20 +116,46 @@ defmodule MobilizonWeb.Resolvers.User do """ @spec create_user(any(), map(), any()) :: tuple() def create_user(_parent, args, _resolution) do - with {:registrations_open, true} <- - {:registrations_open, Config.instance_registrations_open?()}, + with :registration_ok <- check_registration_config(args), {:ok, %User{} = user} <- Users.register(args) do Activation.send_confirmation_email(user, Map.get(args, :locale, "en")) {:ok, user} else - {:registrations_open, false} -> + :registration_closed -> {:error, "Registrations are not enabled"} + :not_whitelisted -> + {:error, "Your email is not on the whitelist"} + error -> error end end + @spec check_registration_config(map()) :: atom() + defp check_registration_config(%{email: email}) do + cond do + Config.instance_registrations_open?() -> + :registration_ok + + Config.instance_registrations_whitelist?() -> + check_white_listed_email?(email) + + true -> + :registration_closed + end + end + + @spec check_white_listed_email?(String.t()) :: :registration_ok | :not_whitelisted + defp check_white_listed_email?(email) do + [_, domain] = String.split(email, "@", parts: 2, trim: true) + + if domain in Config.instance_registrations_whitelist() or + email in Config.instance_registrations_whitelist(), + do: :registration_ok, + else: :not_whitelisted + end + @doc """ Validate an user, get its actor and a token """ diff --git a/lib/mobilizon_web/schema/config.ex b/lib/mobilizon_web/schema/config.ex index 91f9a97d1..46f9fa233 100644 --- a/lib/mobilizon_web/schema/config.ex +++ b/lib/mobilizon_web/schema/config.ex @@ -13,6 +13,7 @@ defmodule MobilizonWeb.Schema.ConfigType do field(:description, :string) field(:registrations_open, :boolean) + field(:registrations_whitelist, :boolean) field(:demo_mode, :boolean) field(:country_code, :string) field(:location, :lonlat) diff --git a/schema.graphql b/schema.graphql index 07dc7f638..b8ca321aa 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,5 @@ # source: http://localhost:4000/api -# timestamp: Wed Dec 11 2019 15:24:29 GMT+0100 (heure normale d’Europe centrale) +# timestamp: Tue Dec 17 2019 11:21:37 GMT+0100 (heure normale d’Europe centrale) schema { query: RootQueryType @@ -256,6 +256,7 @@ type Config { maps: Maps name: String registrationsOpen: Boolean + registrationsWhitelist: Boolean } type Dashboard { @@ -996,70 +997,6 @@ enum ReportStatus { } type RootMutationType { - """Change default actor for user""" - changeDefaultActor(preferredUsername: String!): User - - """Create a new person for user""" - createPerson( - """ - The avatar for the profile, either as an object or directly the ID of an existing Picture - """ - avatar: PictureInput - - """ - The banner for the profile, either as an object or directly the ID of an existing Picture - """ - banner: PictureInput - - """The displayed name for the new profile""" - name: String = "" - preferredUsername: String! - - """The summary for the new profile""" - summary: String = "" - ): Person - - """Upload a picture""" - uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture - - """Delete an event""" - deleteEvent(actorId: ID!, eventId: ID!): DeletedObject - - """Create a note on a report""" - createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote - - """Accept a relay subscription""" - acceptRelay(address: String!): Follower - - """Delete a feed token""" - deleteFeedToken(token: String!): DeletedFeedToken - - """Validate an user after registration""" - validateUser(token: String!): Login - - """Resend registration confirmation token""" - resendConfirmationEmail(email: String!, locale: String): String - - """Update an identity""" - updatePerson( - """ - The avatar for the profile, either as an object or directly the ID of an existing Picture - """ - avatar: PictureInput - - """ - The banner for the profile, either as an object or directly the ID of an existing Picture - """ - banner: PictureInput - id: ID! - - """The displayed name for this profile""" - name: String - - """The summary for this profile""" - summary: String - ): Person - """Create an event""" createEvent( beginsOn: DateTime! @@ -1087,47 +1024,24 @@ type RootMutationType { visibility: EventVisibility = PUBLIC ): Event - """Register a first profile on registration""" - registerPerson( - """ - The avatar for the profile, either as an object or directly the ID of an existing Picture - """ - avatar: PictureInput + """Create a Feed Token""" + createFeedToken(actorId: ID): FeedToken - """ - The banner for the profile, either as an object or directly the ID of an existing Picture - """ - banner: PictureInput + """Create a report""" + createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report - """The email from the user previously created""" - email: String! - - """The displayed name for the new profile""" - name: String = "" - preferredUsername: String! - - """The summary for the new profile""" - summary: String = "" - ): Person - - """Accept a participation""" - updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant - - """Delete a group""" - deleteGroup(actorId: ID!, groupId: ID!): DeletedObject + """Change default actor for user""" + changeDefaultActor(preferredUsername: String!): User deleteComment(actorId: ID!, commentId: ID!): Comment - """Create an user""" - createUser(email: String!, locale: String, password: String!): User + """Delete an event""" + deleteEvent(actorId: ID!, eventId: ID!): DeletedObject - """Leave an event""" - leaveEvent(actorId: ID!, eventId: ID!): DeletedParticipant + """Join an event""" + joinEvent(actorId: ID!, eventId: ID!): Participant - """Refresh a token""" - refreshToken(refreshToken: String!): RefreshedToken - - """Join a group""" - joinGroup(actorId: ID!, groupId: ID!): Member + """Reset user password""" + resetPassword(locale: String = "en", password: String!, token: String!): Login """Update an event""" updateEvent( @@ -1156,42 +1070,75 @@ type RootMutationType { visibility: EventVisibility = PUBLIC ): Event - """Reset user password""" - resetPassword(locale: String = "en", password: String!, token: String!): Login + """Change an user password""" + changePassword(newPassword: String!, oldPassword: String!): User - """Create a report""" - createReport(commentsIds: [ID] = [""], content: String, eventId: ID, forward: Boolean = false, reportedId: ID!, reporterId: ID!): Report + """Delete a feed token""" + deleteFeedToken(token: String!): DeletedFeedToken - """Update a report""" - updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report - deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject + """Register a first profile on registration""" + registerPerson( + """ + The avatar for the profile, either as an object or directly the ID of an existing Picture + """ + avatar: PictureInput - """Delete a relay subscription""" - removeRelay(address: String!): Follower + """ + The banner for the profile, either as an object or directly the ID of an existing Picture + """ + banner: PictureInput - """Create a comment""" - createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment + """The email from the user previously created""" + email: String! + + """The displayed name for the new profile""" + name: String = "" + preferredUsername: String! + + """The summary for the new profile""" + summary: String = "" + ): Person """Delete an identity""" deletePerson(id: ID!): Person - """Reject a relay subscription""" - rejectRelay(address: String!): Follower + """Leave an event""" + leaveEvent(actorId: ID!, eventId: ID!): DeletedParticipant + + """Accept a participation""" + updateParticipation(id: ID!, moderatorActorId: ID!, role: ParticipantRoleEnum!): Participant """Login an user""" login(email: String!, password: String!): Login - """Leave an event""" - leaveGroup(actorId: ID!, groupId: ID!): DeletedMember + """Upload a picture""" + uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture - """Change an user password""" - changePassword(newPassword: String!, oldPassword: String!): User + """Create a new person for user""" + createPerson( + """ + The avatar for the profile, either as an object or directly the ID of an existing Picture + """ + avatar: PictureInput - """Add a relay subscription""" - addRelay(address: String!): Follower + """ + The banner for the profile, either as an object or directly the ID of an existing Picture + """ + banner: PictureInput - """Join an event""" - joinEvent(actorId: ID!, eventId: ID!): Participant + """The displayed name for the new profile""" + name: String = "" + preferredUsername: String! + + """The summary for the new profile""" + summary: String = "" + ): Person + + """Send a link through email to reset user password""" + sendResetPassword(email: String!, locale: String): String + + """Create a comment""" + createComment(actorId: ID!, eventId: ID, inReplyToCommentId: ID, text: String!): Comment """Create a group""" createGroup( @@ -1218,11 +1165,65 @@ type RootMutationType { summary: String = "" ): Group - """Send a link through email to reset user password""" - sendResetPassword(email: String!, locale: String): String + """Update a report""" + updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report - """Create a Feed Token""" - createFeedToken(actorId: ID): FeedToken + """Create an user""" + createUser(email: String!, locale: String, password: String!): User + deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject + + """Update an identity""" + updatePerson( + """ + The avatar for the profile, either as an object or directly the ID of an existing Picture + """ + avatar: PictureInput + + """ + The banner for the profile, either as an object or directly the ID of an existing Picture + """ + banner: PictureInput + id: ID! + + """The displayed name for this profile""" + name: String + + """The summary for this profile""" + summary: String + ): Person + + """Join a group""" + joinGroup(actorId: ID!, groupId: ID!): Member + + """Delete a group""" + deleteGroup(actorId: ID!, groupId: ID!): DeletedObject + + """Resend registration confirmation token""" + resendConfirmationEmail(email: String!, locale: String): String + + """Delete a relay subscription""" + removeRelay(address: String!): Follower + + """Refresh a token""" + refreshToken(refreshToken: String!): RefreshedToken + + """Add a relay subscription""" + addRelay(address: String!): Follower + + """Reject a relay subscription""" + rejectRelay(address: String!): Follower + + """Leave an event""" + leaveGroup(actorId: ID!, groupId: ID!): DeletedMember + + """Validate an user after registration""" + validateUser(token: String!): Login + + """Create a note on a report""" + createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote + + """Accept a relay subscription""" + acceptRelay(address: String!): Follower } """ diff --git a/test/mobilizon_web/resolvers/user_resolver_test.exs b/test/mobilizon_web/resolvers/user_resolver_test.exs index b35ec9d97..643b714ae 100644 --- a/test/mobilizon_web/resolvers/user_resolver_test.exs +++ b/test/mobilizon_web/resolvers/user_resolver_test.exs @@ -2,11 +2,10 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do use MobilizonWeb.ConnCase import Mobilizon.Factory - import Mock use Bamboo.Test - alias Mobilizon.{Actors, Config, Users} + alias Mobilizon.{Actors, Users} alias Mobilizon.Actors.Actor alias Mobilizon.Service.Users.ResetPassword alias Mobilizon.Users @@ -321,6 +320,123 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do assert hd(json_response(res, 200)["errors"])["message"] == "This email is already used." end + test "create_user/3 doesn't allow registration when registration is closed", %{conn: conn} do + Mobilizon.Config.put([:instance, :registrations_open], false) + Mobilizon.Config.put([:instance, :registration_email_whitelist], []) + + mutation = """ + mutation createUser($email: String!, $password: String!) { + createUser( + email: $email, + password: $password, + ) { + id, + email + } + } + """ + + res = + conn + |> AbsintheHelpers.graphql_query( + query: mutation, + variables: %{email: @user_creation.email, password: @user_creation.password} + ) + + assert hd(res["errors"])["message"] == "Registrations are not enabled" + Mobilizon.Config.put([:instance, :registrations_open], true) + end + + test "create_user/3 doesn't allow registration when user email is not on the whitelist", %{ + conn: conn + } do + Mobilizon.Config.put([:instance, :registrations_open], false) + Mobilizon.Config.put([:instance, :registration_email_whitelist], ["random.org"]) + + mutation = """ + mutation createUser($email: String!, $password: String!) { + createUser( + email: $email, + password: $password, + ) { + id, + email + } + } + """ + + res = + conn + |> AbsintheHelpers.graphql_query( + query: mutation, + variables: %{email: @user_creation.email, password: @user_creation.password} + ) + + assert hd(res["errors"])["message"] == "Your email is not on the whitelist" + Mobilizon.Config.put([:instance, :registrations_open], true) + Mobilizon.Config.put([:instance, :registration_email_whitelist], []) + end + + test "create_user/3 allows registration when user email domain is on the whitelist", %{ + conn: conn + } do + Mobilizon.Config.put([:instance, :registrations_open], false) + Mobilizon.Config.put([:instance, :registration_email_whitelist], ["demo.tld"]) + + mutation = """ + mutation createUser($email: String!, $password: String!) { + createUser( + email: $email, + password: $password, + ) { + id, + email + } + } + """ + + res = + conn + |> AbsintheHelpers.graphql_query( + query: mutation, + variables: %{email: @user_creation.email, password: @user_creation.password} + ) + + refute res["errors"] + assert res["data"]["createUser"]["email"] == @user_creation.email + Mobilizon.Config.put([:instance, :registrations_open], true) + Mobilizon.Config.put([:instance, :registration_email_whitelist], []) + end + + test "create_user/3 allows registration when user email is on the whitelist", %{conn: conn} do + Mobilizon.Config.put([:instance, :registrations_open], false) + Mobilizon.Config.put([:instance, :registration_email_whitelist], [@user_creation.email]) + + mutation = """ + mutation createUser($email: String!, $password: String!) { + createUser( + email: $email, + password: $password, + ) { + id, + email + } + } + """ + + res = + conn + |> AbsintheHelpers.graphql_query( + query: mutation, + variables: %{email: @user_creation.email, password: @user_creation.password} + ) + + refute res["errors"] + assert res["data"]["createUser"]["email"] == @user_creation.email + Mobilizon.Config.put([:instance, :registrations_open], true) + Mobilizon.Config.put([:instance, :registration_email_whitelist], []) + end + test "register_person/3 doesn't register a profile from an unknown email", context do mutation = """ mutation { @@ -450,29 +566,6 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do assert hd(json_response(res, 200)["errors"])["message"] == "Email doesn't fit required format" end - - test "test create_user/3 doesn't create a user when registration is disabled", context do - with_mock Config, instance_registrations_open?: fn -> false end do - mutation = """ - mutation { - createUser( - email: "#{@user_creation.email}", - password: "#{@user_creation.password}", - ) { - id, - email - } - } - """ - - res = - context.conn - |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) - - assert hd(json_response(res, 200)["errors"])["message"] == - "Registrations are not enabled" - end - end end describe "Resolver: Validate an user" do