Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Feat/multi account sites #432

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

## Development setup

The easiest way to get up and running is to [install](https://docs.docker.com/get-docker/) and use Docker for running both Postgres and Clickhouse.
The easiest way to get up and running is to [install](https://docs.docker.com/get-docker/) Docker and [install](https://docs.docker.com/compose/install/) Docker Compose for running both Postgres and Clickhouse.

Make sure Docker, Elixir, Erlang and Node.js are all installed on your development machine.

### Start the environment:

1. Run both `make postgres` and `make clickhouse`.
2. Run `mix deps.get`. This will download the required Elixir dependencies.
2. Run `mix ecto.create`. This will create the required databases in both Postgres and Clickhouse.
3. Run `mix ecto.migrate` to build the database schema.
4. Run `npm ci --prefix assets` to install the required node dependencies.
5. Run `mix phx.server` to start the Phoenix server.
6. The system is now available on `localhost:8000`.
3. Run `mix ecto.create`. This will create the required databases in both Postgres and Clickhouse.
4. Run `mix ecto.migrate` to build the database schema.
5. Run `npm ci --prefix assets` to install the required node dependencies.
6. Run `mix phx.server` to start the Phoenix server.
7. The system is now available on `localhost:8000`.

### Creating an account

Expand Down
3 changes: 3 additions & 0 deletions database.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# database.env
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
22 changes: 22 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: "3"
services:

postgres:
image: "postgres" # use latest official postgres version
env_file: './database.env'
volumes:
- plausible_postgres_data:/var/lib/postgresql/data/ # persist data even if container shuts down
ports:
- 5432:5432

# docker run -p 8123:8123 --ulimit nofile=262144:262144 --volume=$$HOME/clickhouse_db_vol:/var/lib/clickhouse yandex/clickhouse-server
clickhouse:
image: "yandex/clickhouse-server" # use latest official postgres version
volumes:
- plausible_clickhouse_data:/var/lib/clickhouse
ports:
- 8123:8123

volumes:
plausible_postgres_data:
plausible_clickhouse_data:
8 changes: 7 additions & 1 deletion lib/plausible/site/membership.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ defmodule Plausible.Site.Membership do
schema "site_memberships" do
belongs_to :site, Plausible.Site
belongs_to :user, Plausible.Auth.User
field :role, :string

timestamps()
end

def changeset(user, attrs) do
user
|> cast(attrs, [:user_id, :site_id])
|> cast(attrs, [:user_id, :site_id, :role])
|> validate_required([:user_id, :site_id])
end

def validate_role(changeset) do
changeset
|> validate_inclusion(:role, ["admin", "viewer"])
end
end
5 changes: 5 additions & 0 deletions lib/plausible/site/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Plausible.Site do
field :domain, :string
field :timezone, :string
field :public, :boolean
field :owner_id, :integer

many_to_many :members, User, join_through: Plausible.Site.Membership
has_one :google_auth, GoogleAuth
Expand All @@ -27,6 +28,10 @@ defmodule Plausible.Site do
|> clean_domain
end

def make_owner(site, user_id) do
change(site, owner_id: user_id)
end

def make_public(site) do
change(site, public: true)
end
Expand Down
22 changes: 20 additions & 2 deletions lib/plausible/sites.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ defmodule Plausible.Sites do
)
end

def members(site_id) when is_integer(site_id) do
from(u in Plausible.Auth.User,
join: sm in Plausible.Site.Membership,
on: sm.user_id == u.id,
where: sm.site_id == ^site_id,
select: [u, sm.role]
)
|> Repo.all()
|> Enum.map(fn [user, role] -> Map.put(user, :role, role) end)
end

def has_goals?(site) do
Repo.exists?(
from g in Plausible.Goal,
Expand All @@ -22,8 +33,8 @@ defmodule Plausible.Sites do

def is_owner?(user_id, site) do
Repo.exists?(
from sm in Plausible.Site.Membership,
where: sm.user_id == ^user_id and sm.site_id == ^site.id
from s in Plausible.Site,
where: s.owner_id == ^user_id and s.id == ^site.id
)
end

Expand All @@ -34,4 +45,11 @@ defmodule Plausible.Sites do
})
|> Repo.insert()
end

def is_admin?(user_id, site) do
Repo.exists?(
from sm in Plausible.Site.Membership,
where: sm.user_id == ^user_id and sm.site_id == ^site.id
)
end
end
135 changes: 129 additions & 6 deletions lib/plausible_web/controllers/site_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ defmodule PlausibleWeb.SiteController do
end

def settings_general(conn, %{"website" => website}) do
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:custom_domain)
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:custom_domain)

conn
|> assign(:skip_plausible_tracking, true)
Expand All @@ -116,6 +117,21 @@ defmodule PlausibleWeb.SiteController do
)
end

def settings_members(conn, %{"website" => website}) do
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:custom_domain)
|> Repo.preload(members: [:site_memberships])

conn
|> assign(:skip_plausible_tracking, true)
|> render("settings_members.html",
site: site,
changeset: Plausible.Site.changeset(site, %{}),
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
)
end

def settings_goals(conn, %{"website" => website}) do
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
goals = Goals.for_site(site.domain)
Expand All @@ -130,7 +146,8 @@ defmodule PlausibleWeb.SiteController do
end

def settings_search_console(conn, %{"website" => website}) do
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:google_auth)

search_console_domains =
Expand Down Expand Up @@ -161,12 +178,16 @@ defmodule PlausibleWeb.SiteController do
end

def settings_custom_domain(conn, %{"website" => website}) do
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(:custom_domain)

conn
|> assign(:skip_plausible_tracking, true)
|> render("settings_custom_domain.html", site: site, layout: {PlausibleWeb.LayoutView, "site_settings.html"})
|> render("settings_custom_domain.html",
site: site,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
)
end

def settings_danger_zone(conn, %{"website" => website}) do
Expand Down Expand Up @@ -205,8 +226,60 @@ defmodule PlausibleWeb.SiteController do
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/search-console")
end

def delete_member(conn, %{"website" => website}) do
current_user = conn.assigns[:current_user]

site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(members: [:site_memberships])

Repo.delete!(site.google_auth)

conn
|> put_flash(:success, "Google account unlinked from Plausible")
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/search-console")
end

def invite_member(conn, %{"website" => website, "new_user" => new_user}) do
current_user = conn.assigns[:current_user]
new_user = Map.get(new_user, "email")

site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(members: [:site_memberships])

if Sites.is_admin?(current_user.id, site) do
conn
|> put_flash(:success, "#{new_user} was invited to Plausible")
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/members")
else
conn
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/members")
end
end

def update_memberships(conn, %{"website" => website, "site" => site_params}) do
site =
Sites.get_for_user!(conn.assigns[:current_user].id, website)
|> Repo.preload(members: [:site_memberships])

# get members, filter out owner
membership_updates = parse_membership_updates(site_params)

role_updates(site.members, membership_updates, site)
|> Enum.each(&Repo.update/1)

site_session_key = "authorized_site__" <> site.domain

conn
|> put_session(site_session_key, nil)
|> put_flash(:success, "Your site settings have been saved")
|> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/members")
end

def update_settings(conn, %{"website" => website, "site" => site_params}) do
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)

changeset = site |> Plausible.Site.changeset(site_params)
res = changeset |> Repo.update()

Expand Down Expand Up @@ -493,7 +566,10 @@ defmodule PlausibleWeb.SiteController do
end

defp insert_site(user_id, params) do
site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params)
site_changeset =
%Plausible.Site{}
|> Plausible.Site.changeset(params)
|> Plausible.Site.make_owner(user_id)

Ecto.Multi.new()
|> Ecto.Multi.insert(:site, site_changeset)
Expand All @@ -508,4 +584,51 @@ defmodule PlausibleWeb.SiteController do
end)
|> Repo.transaction()
end

defp parse_membership_updates(site_params) do
site_params
|> Map.get("members", [])
|> Enum.map(fn {_index, %{"site_memberships" => data}} ->
data |> Map.get("0")
end)
|> Enum.reduce(
%{},
fn
nil, acc ->
acc

data, acc ->
{id, _} = Map.get(data, "id", nil) |> Integer.parse()
role = Map.get(data, "role", nil)

if(id !== nil && role !== nil) do
Map.put(acc, id, role)
else
acc
end
end
)
end

defp role_updates(members, membership_updates, site) do
members
|> Enum.map(fn member ->
member.site_memberships
|> Enum.filter(fn sm -> sm.site_id === site.id end)
end)
|> List.flatten()
|> Enum.reduce(
[],
fn sm, acc ->
new_role = Map.get(membership_updates, sm.id)

[
sm
|> Plausible.Site.Membership.changeset(%{:role => new_role})
|> Plausible.Site.Membership.validate_role()
| acc
]
end
)
end
end
4 changes: 4 additions & 0 deletions lib/plausible_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ defmodule PlausibleWeb.Router do
get "/:website/settings", SiteController, :settings
get "/:website/settings/general", SiteController, :settings_general
get "/:website/settings/visibility", SiteController, :settings_visibility
get "/:website/settings/members", SiteController, :settings_members
get "/:website/settings/goals", SiteController, :settings_goals
get "/:website/settings/search-console", SiteController, :settings_search_console
get "/:website/settings/email-reports", SiteController, :settings_email_reports
Expand All @@ -156,10 +157,13 @@ defmodule PlausibleWeb.Router do
post "/:website/goals", SiteController, :create_goal
delete "/:website/goals/:id", SiteController, :delete_goal
put "/:website/settings", SiteController, :update_settings
put "/:website/settings/members", SiteController, :update_memberships
post "/:website/settings/members", SiteController, :invite_member
put "/:website/settings/google", SiteController, :update_google_auth
delete "/:website/settings/google", SiteController, :delete_google_auth
delete "/:website", SiteController, :delete_site
delete "/:website/stats", SiteController, :reset_stats
delete "/:website/settings/members/:id", SiteController, :delete_member

get "/share/:slug", StatsController, :shared_link
post "/share/:slug/authenticate", StatsController, :authenticate_shared_link
Expand Down
Loading