diff --git a/lib/nulla/users.ex b/lib/nulla/users.ex new file mode 100644 index 0000000..292db75 --- /dev/null +++ b/lib/nulla/users.ex @@ -0,0 +1,106 @@ +defmodule Nulla.Users do + @moduledoc """ + The Users context. + """ + + import Ecto.Query, warn: false + alias Nulla.Repo + + alias Nulla.Users.User + + @doc """ + Returns the list of users. + + ## Examples + + iex> list_users() + [%User{}, ...] + + """ + def list_users do + Repo.all(User) + end + + @doc """ + Gets a single user. + + Raises `Ecto.NoResultsError` if the User does not exist. + + ## Examples + + iex> get_user!(123) + %User{} + + iex> get_user!(456) + ** (Ecto.NoResultsError) + + """ + def get_user!(id), do: Repo.get!(User, id) + + def get_user_by_username!(username), do: Repo.get_by!(User, username: username) + + @doc """ + Creates a user. + + ## Examples + + iex> create_user(%{field: value}) + {:ok, %User{}} + + iex> create_user(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_user(attrs \\ %{}) do + %User{} + |> User.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a user. + + ## Examples + + iex> update_user(user, %{field: new_value}) + {:ok, %User{}} + + iex> update_user(user, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_user(%User{} = user, attrs) do + user + |> User.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a user. + + ## Examples + + iex> delete_user(user) + {:ok, %User{}} + + iex> delete_user(user) + {:error, %Ecto.Changeset{}} + + """ + def delete_user(%User{} = user) do + Repo.delete(user) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking user changes. + + ## Examples + + iex> change_user(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user(%User{} = user, attrs \\ %{}) do + User.changeset(user, attrs) + end +end diff --git a/lib/nulla/user.ex b/lib/nulla/users/user.ex similarity index 86% rename from lib/nulla/user.ex rename to lib/nulla/users/user.ex index ab732d6..b03f068 100644 --- a/lib/nulla/user.ex +++ b/lib/nulla/users/user.ex @@ -1,4 +1,4 @@ -defmodule Nulla.User do +defmodule Nulla.Users.User do use Ecto.Schema import Ecto.Changeset @@ -13,6 +13,7 @@ defmodule Nulla.User do field :location, :string field :birthday, :date field :fields, :map + field :tags, {:array, :string} field :follow_approval, :boolean, default: false field :is_bot, :boolean, default: false field :is_discoverable, :boolean, default: true @@ -20,7 +21,7 @@ defmodule Nulla.User do field :is_memorial, :boolean, default: false field :private_key, :string field :public_key, :string - field :avater, :string + field :avatar, :string field :banner, :string timestamps(type: :utc_datetime) @@ -29,7 +30,7 @@ defmodule Nulla.User do @doc false def changeset(user, attrs) do user - |> cast(attrs, [:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avater, :banner]) - |> validate_required([:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avater, :banner]) + |> cast(attrs, [:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avatar, :banner]) + |> validate_required([:username, :email, :password, :is_moderator, :realname, :bio, :location, :birthday, :fields, :follow_approval, :is_bot, :is_discoverable, :is_indexable, :is_memorial, :private_key, :public_key, :avatar, :banner]) end end diff --git a/lib/nulla_web/controllers/user_controller.ex b/lib/nulla_web/controllers/user_controller.ex index b152141..76e8b80 100644 --- a/lib/nulla_web/controllers/user_controller.ex +++ b/lib/nulla_web/controllers/user_controller.ex @@ -1,64 +1,21 @@ defmodule NullaWeb.UserController do use NullaWeb, :controller + alias Nulla.Users + alias Nulla.InstanceSettings + alias Nulla.ActivityPub def show(conn, %{"username" => username}) do - accept = get_req_header(conn, "accept") |> List.first() || "" + accept = List.first(get_req_header(conn, "accept")) + instance_settings = InstanceSettings.get_instance_settings!() + domain = instance_settings.domain + user = Users.get_user_by_username!(username) - if String.contains?(accept, "application/activity+json") or String.contains?(accept, "application/ld+json") do + if accept in ["application/activity+json", "application/ld+json"] do conn |> put_resp_content_type("application/activity+json") - |> json(%{ - id: "https://localhost/@#{username}", - type: "Person", - following: "https://localhost/@#{username}/following", - followers: "https://localhost/@#{username}/followers", - inbox: "https://localhost/@#{username}/inbox", - outbox: "https://localhost/@#{username}/outbox", - featured: "https://localhost/@#{username}/collections/featured", - preferredUsername: "miraikumiko", - name: "Mirai Kumiko", - summary: "Lol Kek Cheburek", - url: "https://localhost/@#{username}", - manuallyApprovesFollowers: false, - discoverable: true, - indexable: true, - published: "2025-05-05T00:00:00Z", - memorial: false, - publicKey: %{ - id: "https://localhost/@#{username}#main-key", - owner: "https://localhost/@#{username}", - publicKeyPem: "public key" - }, - tag: [ - %{ - type: "Hashtag", - href: "https://localhost/tags/linux", - name: "#linux" - } - ], - attachment: [ - %{ - type: "PropertyValue", - name: "Website", - value: "https://miraikumiko.com" - } - ], - endpoints: %{ - sharedInbox: "https://localhost/inbox" - }, - icon: %{ - type: "Image", - mediaType: "image/jpeg", - url: "url" - }, - image: %{ - type: "Image", - mediaType: "image/jpeg", - url: "url" - } - }) + |> json(ActivityPub.ap_user(domain, user)) else - render(conn, :show, username: username, layout: false) + render(conn, :show, user: user, domain: domain, layout: false) end end end diff --git a/lib/nulla_web/controllers/user_controller.ex.bak b/lib/nulla_web/controllers/user_controller.ex.bak new file mode 100644 index 0000000..093ba20 --- /dev/null +++ b/lib/nulla_web/controllers/user_controller.ex.bak @@ -0,0 +1,62 @@ +defmodule NullaWeb.UserController do + use NullaWeb, :controller + + alias Nulla.Users + alias Nulla.Users.User + + def index(conn, _params) do + users = Users.list_users() + render(conn, :index, users: users) + end + + def new(conn, _params) do + changeset = Users.change_user(%User{}) + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"user" => user_params}) do + case Users.create_user(user_params) do + {:ok, user} -> + conn + |> put_flash(:info, "User created successfully.") + |> redirect(to: ~p"/users/#{user}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + user = Users.get_user!(id) + render(conn, :show, user: user) + end + + def edit(conn, %{"id" => id}) do + user = Users.get_user!(id) + changeset = Users.change_user(user) + render(conn, :edit, user: user, changeset: changeset) + end + + def update(conn, %{"id" => id, "user" => user_params}) do + user = Users.get_user!(id) + + case Users.update_user(user, user_params) do + {:ok, user} -> + conn + |> put_flash(:info, "User updated successfully.") + |> redirect(to: ~p"/users/#{user}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, user: user, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + user = Users.get_user!(id) + {:ok, _user} = Users.delete_user(user) + + conn + |> put_flash(:info, "User deleted successfully.") + |> redirect(to: ~p"/users") + end +end diff --git a/lib/nulla_web/controllers/user_html.ex b/lib/nulla_web/controllers/user_html.ex index 3c0c6ba..db0df5b 100644 --- a/lib/nulla_web/controllers/user_html.ex +++ b/lib/nulla_web/controllers/user_html.ex @@ -1,10 +1,41 @@ defmodule NullaWeb.UserHTML do - @moduledoc """ - This module contains pages rendered by UserController. - - See the `user_html` directory for all templates available. - """ use NullaWeb, :html embed_templates "user_html/*" + + @doc """ + Renders a user form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def user_form(assigns) + + def format_birthdate(date) do + formatted = Date.to_string(date) |> String.replace("-", "/") + age = Timex.diff(Timex.today(), date, :years) + "#{formatted} (#{age} years old)" + end + + def format_registration_date(date) do + now = Timex.now() + formatted = Date.to_string(date) |> String.replace("-", "/") + + diff = Timex.diff(now, date, :days) + + relative = + cond do + diff == 0 -> "today" + diff == 1 -> "1 day ago" + diff < 30 -> "#{diff} days ago" + diff < 365 -> + months = Timex.diff(now, date, :months) + if months == 1, do: "1 month ago", else: "#{months} months ago" + true -> + years = Timex.diff(now, date, :years) + if years == 1, do: "1 year ago", else: "#{years} years ago" + end + + "#{formatted} (#{relative})" + end end diff --git a/lib/nulla_web/controllers/user_html/edit.html.heex b/lib/nulla_web/controllers/user_html/edit.html.heex new file mode 100644 index 0000000..2f8aa66 --- /dev/null +++ b/lib/nulla_web/controllers/user_html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit User {@user.id} + <:subtitle>Use this form to manage user records in your database. + + +<.user_form changeset={@changeset} action={~p"/users/#{@user}"} /> + +<.back navigate={~p"/users"}>Back to users diff --git a/lib/nulla_web/controllers/user_html/index.html.heex b/lib/nulla_web/controllers/user_html/index.html.heex new file mode 100644 index 0000000..9eca5b7 --- /dev/null +++ b/lib/nulla_web/controllers/user_html/index.html.heex @@ -0,0 +1,23 @@ +<.header> + Listing Users + <:actions> + <.link href={~p"/users/new"}> + <.button>New User + + + + +<.table id="users" rows={@users} row_click={&JS.navigate(~p"/users/#{&1}")}> + <:col :let={user} label="Username">{user.username} + <:action :let={user}> +
+ <.link navigate={~p"/users/#{user}"}>Show +
+ <.link navigate={~p"/users/#{user}/edit"}>Edit + + <:action :let={user}> + <.link href={~p"/users/#{user}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/lib/nulla_web/controllers/user_html/new.html.heex b/lib/nulla_web/controllers/user_html/new.html.heex new file mode 100644 index 0000000..9248fb0 --- /dev/null +++ b/lib/nulla_web/controllers/user_html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New User + <:subtitle>Use this form to manage user records in your database. + + +<.user_form changeset={@changeset} action={~p"/users"} /> + +<.back navigate={~p"/users"}>Back to users diff --git a/lib/nulla_web/controllers/user_html/show.html.heex b/lib/nulla_web/controllers/user_html/show.html.heex index 9f771d6..708842f 100644 --- a/lib/nulla_web/controllers/user_html/show.html.heex +++ b/lib/nulla_web/controllers/user_html/show.html.heex @@ -14,50 +14,56 @@
- Mirai Kumiko - @miraikumiko@nulla.social + {@user.realname} + @{@user.username}@{@domain}
-

Cryptopunk in the past.

-

Silent girl now and admin of this instance.

-
-

Grew up on hacker culture, philosophy, good old movies and anime. That's why I love cyberpunk — modern philosophy and technolization in one bottle. I also use Linux on a first-name basis and can program.

-
-

Can play shooters, chess and other games where strategy and psychological analysis of opponents are important.

-
-

Bunnies and rabbits are superior!

+

{@user.bio}

-
-
-
<.icon name="hero-map-pin" class="mt-0.5 h-5 w-5 flex-none" />
-
Catalonia, Spain
-
-
-
<.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" />
-
2005/02/25 (20 years old)
-
-
-
<.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" />
-
03/20/2025 (2mo ago)
-
-
-
-
-
Website
-
- miraikumiko.com -
-
+
+ <%= if @user.location do %> +
+ <.icon name="hero-map-pin" class="mt-0.5 h-5 w-5 flex-none" /> +
+
<%= @user.location %>
+ <% end %> + + <%= if @user.birthday do %> +
+ <.icon name="hero-cake" class="mt-0.5 h-5 w-5 flex-none" /> +
+
<%= format_birthdate(@user.birthday) %>
+ <% end %> + +
+ <.icon name="hero-calendar" class="mt-0.5 h-5 w-5 flex-none" /> +
+
<%= format_registration_date(@user.inserted_at) %>
+ <%= if @user.fields do %> +
+ <%= for {key, value} <- @user.fields do %> +
<%= key %>
+
+ <%= if Regex.match?(~r{://}, value) do %> + <%= Regex.replace(~r{^\w+://}, value, "") %> + <% else %> + <%= value %> + <% end %> +
+ <% end %> +
+ <% end %>
-
Posts
-
Posts and replies
-
Media
+ Featured + Posts + Posts and replies + Media
diff --git a/lib/nulla_web/controllers/user_html/show.html.heex.bak b/lib/nulla_web/controllers/user_html/show.html.heex.bak new file mode 100644 index 0000000..0cb7aef --- /dev/null +++ b/lib/nulla_web/controllers/user_html/show.html.heex.bak @@ -0,0 +1,15 @@ +<.header> + User {@user.id} + <:subtitle>This is a user record from your database. + <:actions> + <.link href={~p"/users/#{@user}/edit"}> + <.button>Edit user + + + + +<.list> + <:item title="Username">{@user.username} + + +<.back navigate={~p"/users"}>Back to users diff --git a/lib/nulla_web/controllers/user_html/user_form.html.heex b/lib/nulla_web/controllers/user_html/user_form.html.heex new file mode 100644 index 0000000..6871618 --- /dev/null +++ b/lib/nulla_web/controllers/user_html/user_form.html.heex @@ -0,0 +1,9 @@ +<.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:username]} type="text" label="Username" /> + <:actions> + <.button>Save User + + diff --git a/lib/nulla_web/router.ex b/lib/nulla_web/router.ex index 46a699c..4f956cd 100644 --- a/lib/nulla_web/router.ex +++ b/lib/nulla_web/router.ex @@ -19,6 +19,7 @@ defmodule NullaWeb.Router do get "/", PageController, :home get "/@:username", UserController, :show + resources "/users", UserController end # Other scopes may use custom stacks. diff --git a/test/nulla/users_test.exs b/test/nulla/users_test.exs new file mode 100644 index 0000000..653e5cc --- /dev/null +++ b/test/nulla/users_test.exs @@ -0,0 +1,59 @@ +defmodule Nulla.UsersTest do + use Nulla.DataCase + + alias Nulla.Users + + describe "users" do + alias Nulla.Users.User + + import Nulla.UsersFixtures + + @invalid_attrs %{username: nil} + + test "list_users/0 returns all users" do + user = user_fixture() + assert Users.list_users() == [user] + end + + test "get_user!/1 returns the user with given id" do + user = user_fixture() + assert Users.get_user!(user.id) == user + end + + test "create_user/1 with valid data creates a user" do + valid_attrs = %{username: "some username"} + + assert {:ok, %User{} = user} = Users.create_user(valid_attrs) + assert user.username == "some username" + end + + test "create_user/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Users.create_user(@invalid_attrs) + end + + test "update_user/2 with valid data updates the user" do + user = user_fixture() + update_attrs = %{username: "some updated username"} + + assert {:ok, %User{} = user} = Users.update_user(user, update_attrs) + assert user.username == "some updated username" + end + + test "update_user/2 with invalid data returns error changeset" do + user = user_fixture() + assert {:error, %Ecto.Changeset{}} = Users.update_user(user, @invalid_attrs) + assert user == Users.get_user!(user.id) + end + + test "delete_user/1 deletes the user" do + user = user_fixture() + assert {:ok, %User{}} = Users.delete_user(user) + assert_raise Ecto.NoResultsError, fn -> Users.get_user!(user.id) end + end + + test "change_user/1 returns a user changeset" do + user = user_fixture() + assert %Ecto.Changeset{} = Users.change_user(user) + end + end +end diff --git a/test/nulla_web/controllers/user_controller_test.exs b/test/nulla_web/controllers/user_controller_test.exs new file mode 100644 index 0000000..2d67f42 --- /dev/null +++ b/test/nulla_web/controllers/user_controller_test.exs @@ -0,0 +1,84 @@ +defmodule NullaWeb.UserControllerTest do + use NullaWeb.ConnCase + + import Nulla.UsersFixtures + + @create_attrs %{username: "some username"} + @update_attrs %{username: "some updated username"} + @invalid_attrs %{username: nil} + + describe "index" do + test "lists all users", %{conn: conn} do + conn = get(conn, ~p"/users") + assert html_response(conn, 200) =~ "Listing Users" + end + end + + describe "new user" do + test "renders form", %{conn: conn} do + conn = get(conn, ~p"/users/new") + assert html_response(conn, 200) =~ "New User" + end + end + + describe "create user" do + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, ~p"/users", user: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == ~p"/users/#{id}" + + conn = get(conn, ~p"/users/#{id}") + assert html_response(conn, 200) =~ "User #{id}" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/users", user: @invalid_attrs) + assert html_response(conn, 200) =~ "New User" + end + end + + describe "edit user" do + setup [:create_user] + + test "renders form for editing chosen user", %{conn: conn, user: user} do + conn = get(conn, ~p"/users/#{user}/edit") + assert html_response(conn, 200) =~ "Edit User" + end + end + + describe "update user" do + setup [:create_user] + + test "redirects when data is valid", %{conn: conn, user: user} do + conn = put(conn, ~p"/users/#{user}", user: @update_attrs) + assert redirected_to(conn) == ~p"/users/#{user}" + + conn = get(conn, ~p"/users/#{user}") + assert html_response(conn, 200) =~ "some updated username" + end + + test "renders errors when data is invalid", %{conn: conn, user: user} do + conn = put(conn, ~p"/users/#{user}", user: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit User" + end + end + + describe "delete user" do + setup [:create_user] + + test "deletes chosen user", %{conn: conn, user: user} do + conn = delete(conn, ~p"/users/#{user}") + assert redirected_to(conn) == ~p"/users" + + assert_error_sent 404, fn -> + get(conn, ~p"/users/#{user}") + end + end + end + + defp create_user(_) do + user = user_fixture() + %{user: user} + end +end diff --git a/test/support/fixtures/users_fixtures.ex b/test/support/fixtures/users_fixtures.ex new file mode 100644 index 0000000..ae82587 --- /dev/null +++ b/test/support/fixtures/users_fixtures.ex @@ -0,0 +1,20 @@ +defmodule Nulla.UsersFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Nulla.Users` context. + """ + + @doc """ + Generate a user. + """ + def user_fixture(attrs \\ %{}) do + {:ok, user} = + attrs + |> Enum.into(%{ + username: "some username" + }) + |> Nulla.Users.create_user() + + user + end +end