From 276fcb4dc362bd1dce8db6d385f1b061ee61a6e8 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 22 Jan 2024 12:55:11 -0800 Subject: [PATCH 1/3] Add username to users, update register form --- server/.gitignore | 3 +- server/lib/orcasite/accounts/user.ex | 4 +- .../20240122203808_add_username_to_users.exs | 25 ++++ .../20240122203900_populate_username.exs | 11 ++ .../20240122204313_make_username_non_null.exs | 21 +++ .../repo/users/20240122203808.json | 136 ++++++++++++++++++ .../repo/users/20240122204313.json | 136 ++++++++++++++++++ ui/src/components/auth/RegisterForm.tsx | 42 +++++- ui/src/graphql/generated/index.ts | 21 ++- .../mutations/registerWithPassword.graphql | 3 + ui/src/pages/register.tsx | 2 + 11 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 server/priv/repo/migrations/20240122203808_add_username_to_users.exs create mode 100644 server/priv/repo/migrations/20240122203900_populate_username.exs create mode 100644 server/priv/repo/migrations/20240122204313_make_username_non_null.exs create mode 100644 server/priv/resource_snapshots/repo/users/20240122203808.json create mode 100644 server/priv/resource_snapshots/repo/users/20240122204313.json diff --git a/server/.gitignore b/server/.gitignore index 2ab3be75..bd6710e3 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -37,4 +37,5 @@ npm-debug.log config/*.secret.exs -.elixir_ls \ No newline at end of file +.elixir_ls +erl_crash.dump \ No newline at end of file diff --git a/server/lib/orcasite/accounts/user.ex b/server/lib/orcasite/accounts/user.ex index 2a54c0ee..bad31aa0 100644 --- a/server/lib/orcasite/accounts/user.ex +++ b/server/lib/orcasite/accounts/user.ex @@ -11,6 +11,7 @@ defmodule Orcasite.Accounts.User do identities do identity :unique_email, [:email] + identity :unique_username, [:username] end attributes do @@ -21,6 +22,7 @@ defmodule Orcasite.Accounts.User do attribute :last_name, :string attribute :admin, :boolean, default: false, allow_nil?: false attribute :moderator, :boolean, default: false, allow_nil?: false + attribute :username, :string, allow_nil?: false create_timestamp :inserted_at update_timestamp :updated_at @@ -34,7 +36,7 @@ defmodule Orcasite.Accounts.User do identity_field :email sign_in_tokens_enabled? true - register_action_accept [:first_name, :last_name] + register_action_accept [:first_name, :last_name, :username] resettable do sender fn user, token, opts -> diff --git a/server/priv/repo/migrations/20240122203808_add_username_to_users.exs b/server/priv/repo/migrations/20240122203808_add_username_to_users.exs new file mode 100644 index 00000000..b6d416c4 --- /dev/null +++ b/server/priv/repo/migrations/20240122203808_add_username_to_users.exs @@ -0,0 +1,25 @@ +defmodule Orcasite.Repo.Migrations.AddUsernameToUsers do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:users) do + add :username, :text + end + + create unique_index(:users, [:username], name: "users_unique_username_index") + end + + def down do + drop_if_exists unique_index(:users, [:username], name: "users_unique_username_index") + + alter table(:users) do + remove :username + end + end +end \ No newline at end of file diff --git a/server/priv/repo/migrations/20240122203900_populate_username.exs b/server/priv/repo/migrations/20240122203900_populate_username.exs new file mode 100644 index 00000000..7b514d3d --- /dev/null +++ b/server/priv/repo/migrations/20240122203900_populate_username.exs @@ -0,0 +1,11 @@ +defmodule Orcasite.Repo.Migrations.PopulateUsername do + use Ecto.Migration + + def up do + execute("UPDATE users SET username = split_part(email, '@', 1) where username is null;") + flush() + end + + def down do + end +end diff --git a/server/priv/repo/migrations/20240122204313_make_username_non_null.exs b/server/priv/repo/migrations/20240122204313_make_username_non_null.exs new file mode 100644 index 00000000..3ed987de --- /dev/null +++ b/server/priv/repo/migrations/20240122204313_make_username_non_null.exs @@ -0,0 +1,21 @@ +defmodule Orcasite.Repo.Migrations.MakeUsernameNonNull do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:users) do + modify :username, :text, null: false + end + end + + def down do + alter table(:users) do + modify :username, :text, null: true + end + end +end \ No newline at end of file diff --git a/server/priv/resource_snapshots/repo/users/20240122203808.json b/server/priv/resource_snapshots/repo/users/20240122203808.json new file mode 100644 index 00000000..570dcd55 --- /dev/null +++ b/server/priv/resource_snapshots/repo/users/20240122203808.json @@ -0,0 +1,136 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "primary_key?": true, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "citext", + "source": "email", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "hashed_password", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "first_name", + "references": null, + "allow_nil?": true, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "last_name", + "references": null, + "allow_nil?": true, + "primary_key?": false, + "generated?": false + }, + { + "default": "false", + "size": null, + "type": "boolean", + "source": "admin", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "false", + "size": null, + "type": "boolean", + "source": "moderator", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "username", + "references": null, + "allow_nil?": true, + "primary_key?": false, + "generated?": false + }, + { + "default": "fragment(\"now()\")", + "size": null, + "type": "utc_datetime_usec", + "source": "inserted_at", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "fragment(\"now()\")", + "size": null, + "type": "utc_datetime_usec", + "source": "updated_at", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + } + ], + "table": "users", + "hash": "36FFCF828BF6089D8E988E8B6ECBF0B538DEC0F23E0A20F4B75D234086FD8E30", + "repo": "Elixir.Orcasite.Repo", + "identities": [ + { + "name": "unique_email", + "keys": [ + "email" + ], + "base_filter": null, + "index_name": "users_unique_email_index" + }, + { + "name": "unique_username", + "keys": [ + "username" + ], + "base_filter": null, + "index_name": "users_unique_username_index" + } + ], + "schema": null, + "check_constraints": [], + "custom_indexes": [], + "multitenancy": { + "global": null, + "attribute": null, + "strategy": null + }, + "base_filter": null, + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/server/priv/resource_snapshots/repo/users/20240122204313.json b/server/priv/resource_snapshots/repo/users/20240122204313.json new file mode 100644 index 00000000..90fbfbf1 --- /dev/null +++ b/server/priv/resource_snapshots/repo/users/20240122204313.json @@ -0,0 +1,136 @@ +{ + "attributes": [ + { + "default": "nil", + "size": null, + "type": "uuid", + "source": "id", + "references": null, + "allow_nil?": false, + "primary_key?": true, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "citext", + "source": "email", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "hashed_password", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "first_name", + "references": null, + "allow_nil?": true, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "last_name", + "references": null, + "allow_nil?": true, + "primary_key?": false, + "generated?": false + }, + { + "default": "false", + "size": null, + "type": "boolean", + "source": "admin", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "false", + "size": null, + "type": "boolean", + "source": "moderator", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "nil", + "size": null, + "type": "text", + "source": "username", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "fragment(\"now()\")", + "size": null, + "type": "utc_datetime_usec", + "source": "inserted_at", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + }, + { + "default": "fragment(\"now()\")", + "size": null, + "type": "utc_datetime_usec", + "source": "updated_at", + "references": null, + "allow_nil?": false, + "primary_key?": false, + "generated?": false + } + ], + "table": "users", + "hash": "8E5FFA76E9AE3846E854025C36F1C8D6B5BA774D32BFB12876822C1AD1F26D72", + "repo": "Elixir.Orcasite.Repo", + "identities": [ + { + "name": "unique_email", + "keys": [ + "email" + ], + "base_filter": null, + "index_name": "users_unique_email_index" + }, + { + "name": "unique_username", + "keys": [ + "username" + ], + "base_filter": null, + "index_name": "users_unique_username_index" + } + ], + "schema": null, + "check_constraints": [], + "custom_indexes": [], + "multitenancy": { + "global": null, + "strategy": null, + "attribute": null + }, + "base_filter": null, + "custom_statements": [], + "has_create_action": true +} \ No newline at end of file diff --git a/ui/src/components/auth/RegisterForm.tsx b/ui/src/components/auth/RegisterForm.tsx index 6dabe522..1a3ca458 100644 --- a/ui/src/components/auth/RegisterForm.tsx +++ b/ui/src/components/auth/RegisterForm.tsx @@ -16,12 +16,20 @@ export default function RegisterForm({ onSubmit, errors }: RegisterFormProps) { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [email, setEmail] = useState(""); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); const handleSubmit = (event: FormEvent) => { event.preventDefault(); - onSubmit({ firstName, lastName, email, password, passwordConfirmation }); + onSubmit({ + firstName, + lastName, + email, + username, + password, + passwordConfirmation, + }); }; return ( @@ -120,6 +128,38 @@ export default function RegisterForm({ onSubmit, errors }: RegisterFormProps) { }, }} /> + setUsername(event.target.value)} + error={!!errors.find((error) => error?.fields?.includes("username"))} + helperText={ + errors.find((error) => error?.fields?.includes("username"))?.message + } + sx={{ + "& .MuiFormLabel-root": { + color: (theme) => theme.palette.secondary.light, + }, + "& .MuiFormLabel-root.Mui-focused": { + color: (theme) => theme.palette.secondary.dark, + }, + "& .MuiOutlinedInput-root": { + "& fieldset": { + borderColor: (theme) => theme.palette.accent2.main, + }, + "&:hover fieldset": { + borderColor: (theme) => theme.palette.accent2.dark, + }, + "&.Mui-focused fieldset": { + borderColor: (theme) => theme.palette.accent2.dark, + }, + }, + }} + /> ; moderator: Scalars["Boolean"]["output"]; + username: Scalars["String"]["output"]; }; export type UserFilterAdmin = { @@ -838,6 +840,7 @@ export type UserFilterInput = { moderator?: InputMaybe; not?: InputMaybe>; or?: InputMaybe>; + username?: InputMaybe; }; export type UserFilterLastName = { @@ -862,6 +865,17 @@ export type UserFilterModerator = { notEq?: InputMaybe; }; +export type UserFilterUsername = { + eq?: InputMaybe; + greaterThan?: InputMaybe; + greaterThanOrEqual?: InputMaybe; + in?: InputMaybe>; + isNil?: InputMaybe; + lessThan?: InputMaybe; + lessThanOrEqual?: InputMaybe; + notEq?: InputMaybe; +}; + export type CancelCandidateNotificationsMutationVariables = Exact<{ candidateId: Scalars["ID"]["input"]; }>; @@ -939,6 +953,7 @@ export type RegisterWithPasswordMutationVariables = Exact<{ firstName?: InputMaybe; lastName?: InputMaybe; email: Scalars["String"]["input"]; + username: Scalars["String"]["input"]; password: Scalars["String"]["input"]; passwordConfirmation: Scalars["String"]["input"]; }>; @@ -951,6 +966,7 @@ export type RegisterWithPasswordMutation = { __typename?: "User"; id: string; email: string; + username: string; admin: boolean; firstName?: string | null; lastName?: string | null; @@ -1399,13 +1415,14 @@ useNotifyConfirmedCandidateMutation.fetcher = ( NotifyConfirmedCandidateMutationVariables >(NotifyConfirmedCandidateDocument, variables, options); export const RegisterWithPasswordDocument = ` - mutation registerWithPassword($firstName: String, $lastName: String, $email: String!, $password: String!, $passwordConfirmation: String!) { + mutation registerWithPassword($firstName: String, $lastName: String, $email: String!, $username: String!, $password: String!, $passwordConfirmation: String!) { registerWithPassword( - input: {email: $email, password: $password, passwordConfirmation: $passwordConfirmation, firstName: $firstName, lastName: $lastName} + input: {email: $email, username: $username, password: $password, passwordConfirmation: $passwordConfirmation, firstName: $firstName, lastName: $lastName} ) { result { id email + username admin firstName lastName diff --git a/ui/src/graphql/mutations/registerWithPassword.graphql b/ui/src/graphql/mutations/registerWithPassword.graphql index 2b2da59c..90433668 100644 --- a/ui/src/graphql/mutations/registerWithPassword.graphql +++ b/ui/src/graphql/mutations/registerWithPassword.graphql @@ -2,12 +2,14 @@ mutation registerWithPassword( $firstName: String $lastName: String $email: String! + $username: String! $password: String! $passwordConfirmation: String! ) { registerWithPassword( input: { email: $email + username: $username password: $password passwordConfirmation: $passwordConfirmation firstName: $firstName @@ -17,6 +19,7 @@ mutation registerWithPassword( result { id email + username admin firstName lastName diff --git a/ui/src/pages/register.tsx b/ui/src/pages/register.tsx index 50b221cc..e416a11e 100644 --- a/ui/src/pages/register.tsx +++ b/ui/src/pages/register.tsx @@ -51,6 +51,7 @@ const RegisterPage: NextPageWithLayout = () => { firstName, lastName, email, + username, password, passwordConfirmation, }) => @@ -58,6 +59,7 @@ const RegisterPage: NextPageWithLayout = () => { firstName, lastName, email, + username, password, passwordConfirmation, }) From 1db5fdceb7a9fc9c0463094173bdfd6ac5f95f76 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 22 Jan 2024 12:58:46 -0800 Subject: [PATCH 2/3] Update tests with username --- server/test/orcasite_web/graphql/accounts_test.exs | 1 + server/test/orcasite_web/graphql/moderator_test.exs | 2 ++ server/test/support/authentication_helper.ex | 2 ++ server/test/support/graphql_helper.ex | 2 ++ 4 files changed, 7 insertions(+) diff --git a/server/test/orcasite_web/graphql/accounts_test.exs b/server/test/orcasite_web/graphql/accounts_test.exs index ec065cfa..87167f65 100644 --- a/server/test/orcasite_web/graphql/accounts_test.exs +++ b/server/test/orcasite_web/graphql/accounts_test.exs @@ -8,6 +8,7 @@ defmodule OrcasiteWeb.GraphqlTest.AccountsTest do @user_params %{ email: "nonadmin@example.com", + username: "nonadmin", password: "password", password_confirmation: "password", first_name: "Non", diff --git a/server/test/orcasite_web/graphql/moderator_test.exs b/server/test/orcasite_web/graphql/moderator_test.exs index 660ed869..b0015859 100644 --- a/server/test/orcasite_web/graphql/moderator_test.exs +++ b/server/test/orcasite_web/graphql/moderator_test.exs @@ -5,12 +5,14 @@ defmodule OrcasiteWeb.ModeratorTest do @moderator_params %{ email: "moderator@example.com", + username: "moderator", password: "password", password_confirmation: "password" } @user_params %{ email: "user@example.com", + username: "user", password: "password", password_confirmation: "password" } diff --git a/server/test/support/authentication_helper.ex b/server/test/support/authentication_helper.ex index 870b25f7..0fbae469 100644 --- a/server/test/support/authentication_helper.ex +++ b/server/test/support/authentication_helper.ex @@ -33,6 +33,7 @@ defmodule OrcasiteWeb.TestSupport.AuthenticationHelper do end def register_user(conn, %{ + username: username, email: email, password: password, first_name: first_name, @@ -42,6 +43,7 @@ defmodule OrcasiteWeb.TestSupport.AuthenticationHelper do "query" => register_mutation(), "variables" => %{ "email" => email, + "username" => username, "password" => password, "passwordConfirmation" => password, "firstName" => first_name, diff --git a/server/test/support/graphql_helper.ex b/server/test/support/graphql_helper.ex index dc60ff29..16a1adb9 100644 --- a/server/test/support/graphql_helper.ex +++ b/server/test/support/graphql_helper.ex @@ -3,6 +3,7 @@ defmodule OrcasiteWeb.TestSupport.GraphqlHelper do """ mutation registerWithPassword( $email: String!, + $username: String!, $firstName: String, $lastName: String, $password: String!, @@ -10,6 +11,7 @@ defmodule OrcasiteWeb.TestSupport.GraphqlHelper do ) { registerWithPassword(input: { email: $email, + username: $username, firstName: $firstName, lastName: $lastName, password: $password, From 42820c69eea6b5babc300e0d747dc100488cd5ce Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 22 Jan 2024 13:07:16 -0800 Subject: [PATCH 3/3] Add username field to admin index, add constraint for non_empty --- server/lib/orcasite/accounts/user.ex | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/lib/orcasite/accounts/user.ex b/server/lib/orcasite/accounts/user.ex index bad31aa0..727a2de8 100644 --- a/server/lib/orcasite/accounts/user.ex +++ b/server/lib/orcasite/accounts/user.ex @@ -22,7 +22,11 @@ defmodule Orcasite.Accounts.User do attribute :last_name, :string attribute :admin, :boolean, default: false, allow_nil?: false attribute :moderator, :boolean, default: false, allow_nil?: false - attribute :username, :string, allow_nil?: false + + attribute :username, :string do + allow_nil? false + constraints allow_empty?: false + end create_timestamp :inserted_at update_timestamp :updated_at @@ -112,7 +116,17 @@ defmodule Orcasite.Accounts.User do end admin do - table_columns [:id, :email, :first_name, :last_name, :admin, :moderator, :inserted_at] + table_columns [ + :id, + :username, + :email, + :first_name, + :last_name, + :admin, + :moderator, + :inserted_at + ] + actor? true read_actions [:read, :current_user] end