Compare commits
3 Commits
main
...
denizen-ap
Author | SHA1 | Date |
---|---|---|
Nat | ebb83274f5 | |
Nat | e983ef7ce1 | |
Nat | cdbb6646eb |
39
README.md
39
README.md
|
@ -1,35 +1,18 @@
|
||||||
# Hostas
|
# Hostas
|
||||||
|
|
||||||
Hostas is:
|
To start your Phoenix server:
|
||||||
- A swiss-army knife of indieweb & federation protocols
|
|
||||||
- A headless CMS designed to invoke the same developer experience as building a website with a static site generator
|
|
||||||
|
|
||||||
Hostas is very early in development and most of the core functionality has yet to be implemented. The long-term goal is for Hostas to offer optional, native support for a whole bunch of indieweb protocols (primarily ActivityPub and Webmentions, see the roadmap for details).
|
* Run `mix setup` to install and setup dependencies
|
||||||
|
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
|
||||||
|
|
||||||
## What is "SSG-style website construction?"
|
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
|
||||||
|
|
||||||
* Your unbuilt website is a directory of template files and subdirectories
|
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
|
||||||
* Paths from the website's root directory are equivalent to how they're accessed when served (i.e. /path/to/page.eex becomes example.com/path/to/page.html)
|
|
||||||
* Pages should be entirely static assets as much as possible in this context
|
|
||||||
|
|
||||||
## Roadmap
|
## Learn more
|
||||||
Immediate-term goals:
|
|
||||||
- Core functionality
|
|
||||||
* User authentication
|
|
||||||
* Content streams
|
|
||||||
* SSG-style website construction
|
|
||||||
- The "hostapi" native API
|
|
||||||
|
|
||||||
Planned protocol support:
|
* Official website: https://www.phoenixframework.org/
|
||||||
- ActivityPub
|
* Guides: https://hexdocs.pm/phoenix/overview.html
|
||||||
- Webmentions
|
* Docs: https://hexdocs.pm/phoenix
|
||||||
|
* Forum: https://elixirforum.com/c/phoenix-forum
|
||||||
Possible support in the future:
|
* Source: https://github.com/phoenixframework/phoenix
|
||||||
- OStatus
|
|
||||||
- Zot
|
|
||||||
- Diaspora
|
|
||||||
|
|
||||||
Interfaces:
|
|
||||||
- Mastodon API
|
|
||||||
- Misskey and derivatives' API
|
|
||||||
- Micropub & Microsub
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
defmodule Hostas.Denizen do
|
defmodule Hostas.Denizen do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query, only: [from: 2]
|
||||||
|
|
||||||
|
alias Hostas.Denizen
|
||||||
|
alias Hostas.Repo
|
||||||
|
|
||||||
schema "denizens" do
|
schema "denizens" do
|
||||||
field :handle, :string
|
field :handle, :string
|
||||||
|
@ -27,6 +31,46 @@ defmodule Hostas.Denizen do
|
||||||
%{password_hash: hash} = Bcrypt.add_hash(password)
|
%{password_hash: hash} = Bcrypt.add_hash(password)
|
||||||
change(changeset, password: hash)
|
change(changeset, password: hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp hash_password(changeset), do: changeset
|
defp hash_password(changeset), do: changeset
|
||||||
|
|
||||||
|
defp query_by_handle(handle) do
|
||||||
|
from d in Denizen, where: d.handle == ^handle
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a denizen. Returns the result of the Ecto.Repo insertion
|
||||||
|
"""
|
||||||
|
def create(denizen) do
|
||||||
|
Repo.insert(changeset(%Denizen{}, denizen))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a denizen
|
||||||
|
"""
|
||||||
|
def get(handle) do
|
||||||
|
case query_by_handle(handle) |> Repo.one do
|
||||||
|
nil -> {:error, :not_found}
|
||||||
|
struct -> {:ok, struct}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
deactivates a denizen's account. This should mark their account as deactivated
|
||||||
|
and replace all their content with tombstones
|
||||||
|
"""
|
||||||
|
def deactivate(handle) do
|
||||||
|
handle
|
||||||
|
|> query_by_handle
|
||||||
|
|> Repo.delete_all
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Modifies all the denizen's fields except `:id`, replacing them with the
|
||||||
|
corresponding values in the map
|
||||||
|
"""
|
||||||
|
def update(%{"handle" => handle} = update_map) do
|
||||||
|
Repo.update(changeset(get(handle), update_map))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,8 @@ defmodule HostasWeb do
|
||||||
import HostasWeb.Gettext
|
import HostasWeb.Gettext
|
||||||
|
|
||||||
unquote(verified_routes())
|
unquote(verified_routes())
|
||||||
|
|
||||||
|
import HostasWeb.ControllerHelpers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule HostasWeb.ControllerHelpers do
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Mark an endpoint as unimplemented
|
||||||
|
"""
|
||||||
|
def unimplemented(conn) do
|
||||||
|
conn
|
||||||
|
|> send_resp(501, "")
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule HostasWeb.DenizenController do
|
||||||
|
# import Ecto.Query, only: [from: 2]
|
||||||
|
use HostasWeb, :controller
|
||||||
|
|
||||||
|
# alias Hostas.Repo
|
||||||
|
# alias Hostas.Denizen
|
||||||
|
# alias Hostas.Token
|
||||||
|
|
||||||
|
def register(conn, _params) do
|
||||||
|
unimplemented(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deactivate(conn, _params) do
|
||||||
|
unimplemented(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
unimplemented(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(conn, _params) do
|
||||||
|
unimplemented(conn)
|
||||||
|
end
|
||||||
|
end
|
|
@ -35,6 +35,12 @@ defmodule HostasWeb.Router do
|
||||||
get "/auth/token", Auth.TokenController, :verify
|
get "/auth/token", Auth.TokenController, :verify
|
||||||
delete "/auth/token/:id", Auth.TokenController, :revoke
|
delete "/auth/token/:id", Auth.TokenController, :revoke
|
||||||
get "/auth/token/:id/renew", Auth.TokenController, :renew
|
get "/auth/token/:id/renew", Auth.TokenController, :renew
|
||||||
|
|
||||||
|
post "/denizen", DenizenController, :register
|
||||||
|
delete "/denizen/:id", DenizenController, :remove
|
||||||
|
get "/denizen/:id", DenizenController, :show
|
||||||
|
patch "/denizen/:id", DenizenController, :update
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
defmodule HostasWeb.Auth.DenizenControllerTest do
|
||||||
|
use HostasWeb.ConnCase
|
||||||
|
|
||||||
|
# For testing with Ecto
|
||||||
|
import Ecto.Query, only: [from: 2]
|
||||||
|
alias Hostas.Repo
|
||||||
|
|
||||||
|
alias Hostas.Denizen
|
||||||
|
alias Hostas.Token
|
||||||
|
|
||||||
|
@denizen %{
|
||||||
|
"handle" => "test",
|
||||||
|
"name" => "Test Denizen",
|
||||||
|
"password" => "password",
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_all %{conn: conn} do
|
||||||
|
denizen1 = %Denizen{}
|
||||||
|
|> Denizen.changeset(%{handle: "denizen1", name: "Test Denizen 1", password: "password"})
|
||||||
|
|> Repo.insert!
|
||||||
|
|
||||||
|
denizen1 = Denizen.create(%{handle: "denizen1", name: "Test Denizen 1", password: "password"})
|
||||||
|
denizen2 = Denizen.create(%{handle: "denizen2", name: "Test Denizen 2", password: "password"})
|
||||||
|
|
||||||
|
{:ok, token} = Token.new(denizen1.id)
|
||||||
|
|
||||||
|
# Facilitate requests to protected routes
|
||||||
|
conn = put_req_header(conn, "authorization", "Bearer #{struct.token}")
|
||||||
|
|
||||||
|
%{denizen1: denizen1, denizen2: denizen2, token: token, conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "register denizen" do
|
||||||
|
test "succeeds", %{conn: conn} do
|
||||||
|
conn = post(conn, ~p"/hostapi/denizen/", @denizen)
|
||||||
|
|
||||||
|
assert json_response(conn, 201)["handle"] == @denizen["handle"]
|
||||||
|
assert json_response(conn, 201)["name"] == @denizen["name"]
|
||||||
|
assert json_response(conn, 201)["id"] == @denizen["id"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because duplicate handle", %{conn: conn, denizen1: pre_existing_denizen} do
|
||||||
|
pre_existing_handle = pre_existing_denizen["handle"]
|
||||||
|
conn = post(
|
||||||
|
conn,
|
||||||
|
~p"/hostapi/denizen/",
|
||||||
|
Map.put(@denizen, "handle", pre_existing_handle)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json_response(conn, 422)["error"] ==
|
||||||
|
"Denizen with handle #{pre_existing_handle} already exists"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "show denizen" do
|
||||||
|
test "succeeds", %{conn: conn, denizen1: denizen} do
|
||||||
|
%{handle: handle} = denizen
|
||||||
|
conn = get(conn, ~p"/hostapi/denizen/#{handle}")
|
||||||
|
assert json_response(conn, 200) == Map.delete(denizen, :password)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because handle is unknown", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/hostapi/denizen/unknown_denizen")
|
||||||
|
assert json_response(conn, 404)["error"] == "No denizen corresponding to given handle"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "deactivate denizen" do
|
||||||
|
test "returns 201 when done by same denizen", %{conn: conn, denizen1: denizen} do
|
||||||
|
%{handle: handle} = denizen
|
||||||
|
conn = delete(conn, ~p"/hostapi/denizen/#{handle}")
|
||||||
|
assert json_response(conn, 201)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "succeeds", %{conn: conn, denizen1: denizen} do
|
||||||
|
%{handle: handle} = denizen
|
||||||
|
conn = delete(conn, ~p"/hostapi/denizen/#{handle}")
|
||||||
|
assert json_response(conn, 201)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because denizens can't deactivate one another", %{conn: conn, denizen2: denizen2} do
|
||||||
|
conn = delete(conn, ~p"/hostapi/denizen/#{denizen2.handle}")
|
||||||
|
assert json_response(conn, 403)["error"] == "Regular denizens cannot deactivate other denizens"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because referenced denizen doesn't exist", %{conn: conn, denizen1: denizen} do
|
||||||
|
conn = delete(conn, ~p"/hostapi/denizen/unknown_denizen")
|
||||||
|
assert json_response(conn, 403)["error"] == "Regular denizens cannot deactivate other denizens"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update denizen" do
|
||||||
|
test "succeeds", %{conn: conn, denizen1: denizen} do
|
||||||
|
update = Map.put(denizen, :name, "Updated Name")
|
||||||
|
conn = patch(conn, ~p"/hostapi/denizen/#{denizen.handle}", update)
|
||||||
|
|
||||||
|
assert json_response(conn, 200) == update
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because denizen can't modify someone else's account", %{conn: conn, denizen2: denizen2} do
|
||||||
|
conn = patch(conn, ~p"/hostapi/denizen/#{denizen.handle}", %{})
|
||||||
|
assert json_response(conn, 403)["error"] == "Regular denizens cannot update other denizens"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fails because denizen doesn't exist", %{conn: conn} do
|
||||||
|
conn = patch(conn, ~p"/hostapi/denizen/unknown_denizen", %{})
|
||||||
|
assert json_response(conn, 403)["error"] == "Regular denizens cannot update other denizens"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue