Compare commits
2 Commits
d1db6def95
...
e57fdfcb3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57fdfcb3f | ||
|
|
a0fc306df1 |
@@ -76,3 +76,4 @@ config :phoenix, :plug_init_mode, :runtime
|
||||
config :phoenix_live_view, :debug_heex_annotations, true
|
||||
|
||||
config :wax_, origin: "http://localhost:4000", rp_id: :auto
|
||||
config :boruta, Boruta.Oauth, repo: SsoBsn.Repo, issuer: "http://localhost:4000"
|
||||
@@ -1,4 +1,11 @@
|
||||
{ lib, self, mixRelease, fetchMixDeps, elixir }: mixRelease rec {
|
||||
{
|
||||
lib,
|
||||
self,
|
||||
mixRelease,
|
||||
fetchMixDeps,
|
||||
elixir,
|
||||
}:
|
||||
mixRelease rec {
|
||||
pname = "sso_bsn";
|
||||
version = "0.0.1";
|
||||
|
||||
|
||||
20
flake.nix
20
flake.nix
@@ -1,19 +1,27 @@
|
||||
{
|
||||
description = "A simple OIDC SSO service";
|
||||
|
||||
outputs = { self, nixpkgs, systems }: let
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
eachSystem = f: lib.genAttrs (import systems) (system: f {
|
||||
eachSystem = f:
|
||||
lib.genAttrs (import systems) (system:
|
||||
f {
|
||||
inherit system;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
ownPkgs = self.packages.${system};
|
||||
});
|
||||
in {
|
||||
devShells = eachSystem ({ pkgs, ... }: {
|
||||
default = pkgs.beam.beamLib.callPackage ./shell.nix {};
|
||||
devShells = eachSystem ({pkgs, ...}: {
|
||||
default = pkgs.beam.packages.erlang_26.callPackage ./shell.nix {};
|
||||
});
|
||||
packages = eachSystem ({ pkgs, ... }: {
|
||||
default = pkgs.beam.packages.erlang.callPackage ./default.nix { inherit self; inherit (pkgs) elixir; };
|
||||
packages = eachSystem ({pkgs, ...}: {
|
||||
default = pkgs.beam.packages.erlang_26.callPackage ./default.nix {
|
||||
inherit self;
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,6 +190,9 @@ defmodule SsoBsn.Accounts do
|
||||
end
|
||||
end
|
||||
|
||||
def update_login_time(user) do
|
||||
user |> User.login_changeset() |> Repo.update!()
|
||||
end
|
||||
## Session
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule SsoBsn.Accounts.User do
|
||||
schema "users" do
|
||||
field :username, :string
|
||||
field :confirmed_at, :naive_datetime
|
||||
field :last_login_at, :utc_datetime_usec
|
||||
|
||||
has_many :keys, UserKey
|
||||
|
||||
@@ -57,4 +58,8 @@ defmodule SsoBsn.Accounts.User do
|
||||
user
|
||||
|> Ecto.build_assoc(:keys)
|
||||
end
|
||||
|
||||
def login_changeset(user) do
|
||||
change(user, last_login_at: DateTime.utc_now())
|
||||
end
|
||||
end
|
||||
|
||||
90
lib/sso_bsn_web/controllers/oauth/authorize_controller.ex
Normal file
90
lib/sso_bsn_web/controllers/oauth/authorize_controller.ex
Normal file
@@ -0,0 +1,90 @@
|
||||
defmodule SsoBsnWeb.Oauth.AuthorizeController do
|
||||
@behaviour Boruta.Oauth.AuthorizeApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Oauth.AuthorizeResponse
|
||||
alias Boruta.Oauth.Error
|
||||
alias Boruta.Oauth.ResourceOwner
|
||||
alias SsoBsnWeb.OauthView
|
||||
|
||||
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
|
||||
|
||||
def authorize(%Plug.Conn{} = conn, _params) do
|
||||
current_user = conn.assigns[:current_user]
|
||||
conn = store_user_return_to(conn)
|
||||
|
||||
authorize_response(
|
||||
conn,
|
||||
current_user
|
||||
)
|
||||
end
|
||||
|
||||
defp authorize_response(conn, %_{} = current_user) do
|
||||
conn
|
||||
|> oauth_module().authorize(
|
||||
%ResourceOwner{sub: to_string(current_user.id), username: current_user.email},
|
||||
__MODULE__
|
||||
)
|
||||
end
|
||||
|
||||
defp authorize_response(conn, _params) do
|
||||
redirect_to_login(conn)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def authorize_success(
|
||||
conn,
|
||||
%AuthorizeResponse{} = response
|
||||
) do
|
||||
redirect(conn, external: AuthorizeResponse.redirect_to_url(response))
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def authorize_error(
|
||||
%Plug.Conn{} = conn,
|
||||
%Error{status: :unauthorized}
|
||||
) do
|
||||
redirect_to_login(conn)
|
||||
end
|
||||
|
||||
def authorize_error(
|
||||
conn,
|
||||
%Error{format: format} = error
|
||||
)
|
||||
when not is_nil(format) do
|
||||
conn
|
||||
|> redirect(external: Error.redirect_to_url(error))
|
||||
end
|
||||
|
||||
def authorize_error(
|
||||
conn,
|
||||
%Error{status: status, error: error, error_description: error_description}
|
||||
) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> put_view(OauthView)
|
||||
|> render("error.html", error: error, error_description: error_description)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def preauthorize_success(_conn, _response), do: :ok
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def preauthorize_error(_conn, _response), do: :ok
|
||||
|
||||
defp store_user_return_to(conn) do
|
||||
conn
|
||||
|> put_session(
|
||||
:user_return_to,
|
||||
current_path(conn)
|
||||
)
|
||||
end
|
||||
|
||||
defp redirect_to_login(_conn) do
|
||||
raise """
|
||||
Here occurs the login process. After login, user may be redirected to
|
||||
get_session(conn, :user_return_to)
|
||||
"""
|
||||
end
|
||||
end
|
||||
64
lib/sso_bsn_web/controllers/oauth/introspect_controller.ex
Normal file
64
lib/sso_bsn_web/controllers/oauth/introspect_controller.ex
Normal file
@@ -0,0 +1,64 @@
|
||||
defmodule SsoBsnWeb.Oauth.IntrospectController do
|
||||
@behaviour Boruta.Oauth.IntrospectApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Oauth.Error
|
||||
alias Boruta.Oauth.IntrospectResponse
|
||||
alias SsoBsnWeb.OauthView
|
||||
|
||||
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
|
||||
|
||||
def introspect(%Plug.Conn{} = conn, _params) do
|
||||
conn |> oauth_module().introspect(__MODULE__)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.IntrospectApplication
|
||||
def introspect_success(conn, %IntrospectResponse{} = response) do
|
||||
conn
|
||||
|> put_view(OauthView)
|
||||
|> introspect(response: response)
|
||||
end
|
||||
|
||||
def introspect(%{
|
||||
response: %IntrospectResponse{
|
||||
active: active,
|
||||
client_id: client_id,
|
||||
username: username,
|
||||
scope: scope,
|
||||
sub: sub,
|
||||
iss: iss,
|
||||
exp: exp,
|
||||
iat: iat
|
||||
}
|
||||
}) do
|
||||
case active do
|
||||
true ->
|
||||
%{
|
||||
active: true,
|
||||
client_id: client_id,
|
||||
username: username,
|
||||
scope: scope,
|
||||
sub: sub,
|
||||
iss: iss,
|
||||
exp: exp,
|
||||
iat: iat
|
||||
}
|
||||
|
||||
false ->
|
||||
%{active: false}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@impl Boruta.Oauth.IntrospectApplication
|
||||
def introspect_error(conn, %Error{
|
||||
status: status,
|
||||
error: error,
|
||||
error_description: error_description
|
||||
}) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{error: error, error_description: error_description})
|
||||
end
|
||||
end
|
||||
31
lib/sso_bsn_web/controllers/oauth/revoke_controller.ex
Normal file
31
lib/sso_bsn_web/controllers/oauth/revoke_controller.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule SsoBsnWeb.Oauth.RevokeController do
|
||||
@behaviour Boruta.Oauth.RevokeApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Oauth.Error
|
||||
alias SsoBsnWeb.OauthView
|
||||
|
||||
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
|
||||
|
||||
def revoke(%Plug.Conn{} = conn, _params) do
|
||||
conn |> oauth_module().revoke(__MODULE__)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.RevokeApplication
|
||||
def revoke_success(%Plug.Conn{} = conn) do
|
||||
send_resp(conn, 200, "")
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.RevokeApplication
|
||||
def revoke_error(conn, %Error{
|
||||
status: status,
|
||||
error: error,
|
||||
error_description: error_description
|
||||
}) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> put_view(OauthView)
|
||||
|> json(%{error: error, error_description: error_description})
|
||||
end
|
||||
end
|
||||
38
lib/sso_bsn_web/controllers/oauth/token_controller.ex
Normal file
38
lib/sso_bsn_web/controllers/oauth/token_controller.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule SsoBsnWeb.Oauth.TokenController do
|
||||
@behaviour Boruta.Oauth.TokenApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Oauth.Error
|
||||
alias Boruta.Oauth.TokenResponse
|
||||
|
||||
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
|
||||
|
||||
def token(%Plug.Conn{} = conn, _params) do
|
||||
conn |> oauth_module().token(__MODULE__)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.TokenApplication
|
||||
def token_success(conn, %TokenResponse{} = response) do
|
||||
conn
|
||||
|> put_resp_header("pragma", "no-cache")
|
||||
|> put_resp_header("cache-control", "no-store")
|
||||
|> json(
|
||||
Enum.filter(
|
||||
response,
|
||||
fn
|
||||
{_key, nil} -> false
|
||||
_ -> true
|
||||
end
|
||||
)
|
||||
|> Enum.into(%{})
|
||||
)
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.TokenApplication
|
||||
def token_error(conn, %Error{status: status, error: error, error_description: error_description}) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{error: error, error_description: error_description})
|
||||
end
|
||||
end
|
||||
163
lib/sso_bsn_web/controllers/openid/authorize_controller.ex
Normal file
163
lib/sso_bsn_web/controllers/openid/authorize_controller.ex
Normal file
@@ -0,0 +1,163 @@
|
||||
defmodule SsoBsnWeb.Openid.AuthorizeController do
|
||||
@behaviour Boruta.Oauth.AuthorizeApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Oauth.AuthorizeResponse
|
||||
alias Boruta.Oauth.Error
|
||||
alias Boruta.Oauth.ResourceOwner
|
||||
alias SsoBsnWeb.OauthView
|
||||
alias SsoBsnWeb.UserAuth
|
||||
|
||||
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
|
||||
|
||||
def authorize(%Plug.Conn{} = conn, _params) do
|
||||
conn =
|
||||
conn
|
||||
|> store_user_return_to()
|
||||
|> put_unsigned_request()
|
||||
|
||||
resource_owner = get_resource_owner(conn)
|
||||
|
||||
with {:unchanged, conn} <- prompt_redirection(conn),
|
||||
{:unchanged, conn} <- max_age_redirection(conn, resource_owner),
|
||||
{:unchanged, conn} <- login_redirection(conn) do
|
||||
oauth_module().authorize(conn, resource_owner, __MODULE__)
|
||||
end
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def authorize_success(
|
||||
conn,
|
||||
%AuthorizeResponse{} = response
|
||||
) do
|
||||
redirect(conn, external: AuthorizeResponse.redirect_to_url(response))
|
||||
end
|
||||
|
||||
@impl Boruta.Oauth.AuthorizeApplication
|
||||
def authorize_error(
|
||||
%Plug.Conn{} = conn,
|
||||
%Error{status: :unauthorized, error: :login_required} = error
|
||||
) do
|
||||
redirect(conn, external: Error.redirect_to_url(error))
|
||||
end
|
||||
|
||||
def authorize_error(
|
||||
%Plug.Conn{} = conn,
|
||||
%Error{status: :unauthorized, error: :invalid_resource_owner}
|
||||
) do
|
||||
redirect_to_login(conn)
|
||||
end
|
||||
|
||||
def authorize_error(
|
||||
conn,
|
||||
%Error{
|
||||
format: format
|
||||
} = error
|
||||
)
|
||||
when not is_nil(format) do
|
||||
redirect(conn, external: Error.redirect_to_url(error))
|
||||
end
|
||||
|
||||
def authorize_error(
|
||||
conn,
|
||||
%Error{status: status, error: error, error_description: error_description}
|
||||
) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> put_view(OauthView)
|
||||
|> render("error.html", error: error, error_description: error_description)
|
||||
end
|
||||
|
||||
defp put_unsigned_request(%Plug.Conn{query_params: query_params} = conn) do
|
||||
unsigned_request_params =
|
||||
with request <- Map.get(query_params, "request", ""),
|
||||
{:ok, params} <- Joken.peek_claims(request) do
|
||||
params
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
query_params = Map.merge(query_params, unsigned_request_params)
|
||||
|
||||
%{conn | query_params: query_params}
|
||||
end
|
||||
|
||||
defp store_user_return_to(conn) do
|
||||
# remove prompt and max_age params affecting redirections
|
||||
conn
|
||||
|> put_session(
|
||||
:user_return_to,
|
||||
current_path(conn)
|
||||
|> String.replace(~r/prompt=(login|none)/, "")
|
||||
|> String.replace(~r/max_age=(\d+)/, "")
|
||||
)
|
||||
end
|
||||
|
||||
defp prompt_redirection(%Plug.Conn{query_params: %{"prompt" => "login"}} = conn) do
|
||||
log_out_user(conn)
|
||||
end
|
||||
|
||||
defp prompt_redirection(%Plug.Conn{} = conn), do: {:unchanged, conn}
|
||||
|
||||
defp max_age_redirection(
|
||||
%Plug.Conn{query_params: %{"max_age" => max_age}} = conn,
|
||||
%ResourceOwner{} = resource_owner
|
||||
) do
|
||||
case login_expired?(resource_owner, max_age) do
|
||||
true ->
|
||||
log_out_user(conn)
|
||||
|
||||
false ->
|
||||
{:unchanged, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp max_age_redirection(%Plug.Conn{} = conn, _resource_owner), do: {:unchanged, conn}
|
||||
|
||||
defp login_expired?(%ResourceOwner{last_login_at: last_login_at}, max_age) do
|
||||
now = DateTime.utc_now() |> DateTime.to_unix()
|
||||
|
||||
with "" <> max_age <- max_age,
|
||||
{max_age, _} <- Integer.parse(max_age),
|
||||
true <- now - DateTime.to_unix(last_login_at) >= max_age do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp login_redirection(%Plug.Conn{assigns: %{current_user: _current_user}} = conn) do
|
||||
{:unchanged, conn}
|
||||
end
|
||||
|
||||
defp login_redirection(%Plug.Conn{query_params: %{"prompt" => "none"}} = conn) do
|
||||
{:unchanged, conn}
|
||||
end
|
||||
|
||||
defp login_redirection(%Plug.Conn{} = conn) do
|
||||
redirect_to_login(conn)
|
||||
end
|
||||
|
||||
defp get_resource_owner(conn) do
|
||||
case conn.assigns[:current_user] do
|
||||
nil ->
|
||||
%ResourceOwner{sub: nil}
|
||||
|
||||
current_user ->
|
||||
%ResourceOwner{
|
||||
sub: to_string(current_user.id),
|
||||
username: current_user.email,
|
||||
last_login_at: current_user.last_login_at
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp redirect_to_login(conn) do
|
||||
conn |> redirect(to: ~p"/users/log_in")
|
||||
end
|
||||
|
||||
defp log_out_user(conn) do
|
||||
UserAuth.log_out_user(conn)
|
||||
end
|
||||
end
|
||||
16
lib/sso_bsn_web/controllers/openid/jwks_controller.ex
Normal file
16
lib/sso_bsn_web/controllers/openid/jwks_controller.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule SsoBsnWeb.Openid.JwksController do
|
||||
@behaviour Boruta.Openid.JwksApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
def openid_module, do: Application.get_env(:sso_bsn, :openid_module, Boruta.Openid)
|
||||
|
||||
def jwks_index(conn, _params) do
|
||||
openid_module().jwks(conn, __MODULE__)
|
||||
end
|
||||
|
||||
@impl Boruta.Openid.JwksApplication
|
||||
def jwk_list(conn, jwk_keys) do
|
||||
conn |> json(%{keys: jwk_keys})
|
||||
end
|
||||
end
|
||||
30
lib/sso_bsn_web/controllers/openid/userinfo_controller.ex
Normal file
30
lib/sso_bsn_web/controllers/openid/userinfo_controller.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule SsoBsnWeb.Openid.UserinfoController do
|
||||
@behaviour Boruta.Openid.UserinfoApplication
|
||||
|
||||
use SsoBsnWeb, :controller
|
||||
|
||||
alias Boruta.Openid.UserinfoResponse
|
||||
|
||||
def openid_module, do: Application.get_env(:sso_bsn, :openid_module, Boruta.Openid)
|
||||
|
||||
def userinfo(conn, _params) do
|
||||
openid_module().userinfo(conn, __MODULE__)
|
||||
end
|
||||
|
||||
@impl Boruta.Openid.UserinfoApplication
|
||||
def userinfo_fetched(conn, userinfo_response) do
|
||||
conn
|
||||
|> put_resp_header("content-type", UserinfoResponse.content_type(userinfo_response))
|
||||
|> json(UserinfoResponse.payload(userinfo_response))
|
||||
end
|
||||
|
||||
@impl Boruta.Openid.UserinfoApplication
|
||||
def unauthorized(conn, error) do
|
||||
conn
|
||||
|> put_resp_header(
|
||||
"www-authenticate",
|
||||
"error=\"#{error.error}\", error_description=\"#{error.error_description}\""
|
||||
)
|
||||
|> send_resp(:unauthorized, "")
|
||||
end
|
||||
end
|
||||
@@ -73,4 +73,35 @@ defmodule SsoBsnWeb.Router do
|
||||
|
||||
delete "/users/log_out", UserSessionController, :delete
|
||||
end
|
||||
|
||||
|
||||
# OIDC
|
||||
scope "/oauth", SsoBsnWeb.Oauth do
|
||||
pipe_through :api
|
||||
|
||||
post "/revoke", RevokeController, :revoke
|
||||
post "/token", TokenController, :token
|
||||
post "/introspect", IntrospectController, :introspect
|
||||
end
|
||||
|
||||
|
||||
scope "/openid", SsoBsnWeb.Openid do
|
||||
pipe_through :api
|
||||
|
||||
get "/userinfo", UserinfoController, :userinfo
|
||||
post "/userinfo", UserinfoController, :userinfo
|
||||
get "/jwks", JwksController, :jwks_index
|
||||
end
|
||||
|
||||
scope "/oauth", SsoBsnWeb.Oauth do
|
||||
pipe_through [:browser, :fetch_current_user]
|
||||
|
||||
get "/authorize", AuthorizeController, :authorize
|
||||
end
|
||||
|
||||
scope "/openid", SsoBsnWeb.Openid do
|
||||
pipe_through [:browser, :fetch_current_user]
|
||||
|
||||
get "/authorize", AuthorizeController, :authorize
|
||||
end
|
||||
end
|
||||
|
||||
1
lib/sso_bsn_web/templates/oauth/error.html.eex
Normal file
1
lib/sso_bsn_web/templates/oauth/error.html.eex
Normal file
@@ -0,0 +1 @@
|
||||
<h2><%= @error_description %></h2>
|
||||
@@ -27,6 +27,7 @@ defmodule SsoBsnWeb.UserAuth do
|
||||
"""
|
||||
def log_in_user(conn, user, params \\ %{}) do
|
||||
token = Accounts.generate_user_session_token(user)
|
||||
Accounts.update_login_time(user)
|
||||
user_return_to = get_session(conn, :user_return_to)
|
||||
|
||||
conn
|
||||
@@ -81,7 +82,7 @@ defmodule SsoBsnWeb.UserAuth do
|
||||
conn
|
||||
|> renew_session()
|
||||
|> delete_resp_cookie(@remember_me_cookie)
|
||||
|> redirect(to: ~p"/")
|
||||
|> redirect(to: ~p"/users/log_in")
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
3
mix.exs
3
mix.exs
@@ -47,7 +47,8 @@ defmodule SsoBsn.MixProject do
|
||||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:wax_, "~> 0.6.0"}
|
||||
{:wax_, "~> 0.6.0"},
|
||||
{:boruta, "~> 2.3.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
10
mix.lock
10
mix.lock
@@ -1,6 +1,7 @@
|
||||
%{
|
||||
"asn1_compiler": {:hex, :asn1_compiler, "0.1.1", "64a4e52b59d1f225878445ace2c75cd2245b13a5a81182304fd9dc5acfc8994e", [:mix], [], "hexpm", "c250d24c22f1a3f305d88864400f9ac2df55c6886e1e3a030e2946efeb94695e"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||
"boruta": {:hex, :boruta, "2.3.1", "d33535cd84fb6516b67a04b12fa6af16c3480a059b3d7bf38f988410dff8049a", [:mix], [{:ecto_sql, ">= 3.5.2", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ex_json_schema, "~> 0.6", [hex: :ex_json_schema, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:nebulex, "~> 2.0", [hex: :nebulex, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:puid, "~> 1.0", [hex: :puid, repo: "hexpm", optional: false]}, {:secure_random, "~> 0.5", [hex: :secure_random, repo: "hexpm", optional: false]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "ae06432f70ab8447afc0d64bd404594c0b1452633458ae2377de250ead7bf0d9"},
|
||||
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
|
||||
"cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.8", "933a5f4da3b19ee56539a076076ce4d7716d64efc8db46fd066996a7e46e2bfd", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "176bdf4366956e456bf761b54ad70bc4103d0269ca9558fd7cee93d1b3f116db"},
|
||||
@@ -8,6 +9,7 @@
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"crypto_rand": {:hex, :crypto_rand, "1.0.3", "8773799e3ed124ce8a81feb935fffbeb3b6621ea2b841c2bbaff4fd09752fd66", [:mix], [], "hexpm", "701a76fea71119fe60aea02f50191bf82a60cb687046931e4d6de6acb8da1685"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
|
||||
@@ -16,6 +18,7 @@
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.12.0", "9ee845ac45a76e3c5c0fe65898f3538f5b0969912a95f0beef3d4ae8e63f6a06", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "4eaf8550df1fd0043bcf039a5dce407fd8afc30a115ced173fe6b9815eeedb55"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
||||
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
|
||||
"ex_json_schema": {:hex, :ex_json_schema, "0.10.1", "e03b746b6675a750c0bb1a5cc919f61353f7ab8450977e11ceede20e6180c560", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "66a64e60dadad89914d92f89c7e7906c57de75a8b79ac2480d0d53e1b8096fb0"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"exqlite": {:hex, :exqlite, "0.16.2", "6a0952f8c7cf90eb7fae7bbda2ff8269363048e6a51209f3a09e0fa7d62e1d8c", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "2a83697b306068644a6e7e04687f503f8973016e3ece787ad5cb2942ec7ed733"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
@@ -24,8 +27,11 @@
|
||||
"gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
|
||||
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
|
||||
"nebulex": {:hex, :nebulex, "2.5.2", "2d358813ccb2eeea525e3a29c270ad123d3337e97ed9159d9113cf128108bd4c", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "61a122302cf42fa61eca22515b1df21aaaa1b98cf462f6dd0998de9797aaf1c7"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.9", "9a2b873e2cb3955efdd18ad050f1818af097fa3f5fc3a6aaba666da36bdd3f02", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83e32da028272b4bfd076c61a964e6d2b9d988378df2f1276a0ed21b13b5e997"},
|
||||
@@ -39,7 +45,11 @@
|
||||
"plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},
|
||||
"puid": {:hex, :puid, "1.1.2", "4acf2a576afc5896c393459d259e733ad20dd96c969152fcddb68f35c1f5ba4a", [:mix], [{:crypto_rand, "~> 1.0", [hex: :crypto_rand, repo: "hexpm", optional: false]}], "hexpm", "fbd1691e29e576c4fbf23852f4d256774702ad1f2a91b37e4344f7c278f1ffaa"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"},
|
||||
"shards": {:hex, :shards, "1.1.0", "ed3032e63ae99f0eaa6d012b8b9f9cead48b9a810b3f91aeac266cfc4118eff6", [:make, :rebar3], [], "hexpm", "1d188e565a54a458a7a601c2fd1e74f5cfeba755c5a534239266d28b7ff124c7"},
|
||||
"swoosh": {:hex, :swoosh, "1.14.0", "710e363e114dedb4080b737e0307f5410887ffc9a239f818231e5618b6b84e1b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dccfc986ac99c18345ab3e1a8b934b2d817fd6d59a2494f0af78502184c71025"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
|
||||
87
priv/repo/migrations/20231105011324_create_boruta.exs
Normal file
87
priv/repo/migrations/20231105011324_create_boruta.exs
Normal file
@@ -0,0 +1,87 @@
|
||||
defmodule SsoBsn.Repo.Migrations.CreateBoruta do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:oauth_clients, primary_key: false) do
|
||||
add :id, :uuid, primary_key: true
|
||||
add :name, :string, default: "", null: false
|
||||
add :secret, :string, null: false
|
||||
add :redirect_uris, {:array, :string}, default: [], null: false
|
||||
add :scope, :string
|
||||
add :authorize_scope, :boolean, default: false, null: false
|
||||
|
||||
add :supported_grant_types, {:array, :string},
|
||||
default: [
|
||||
"client_credentials",
|
||||
"password",
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"implicit",
|
||||
"revoke",
|
||||
"introspect"
|
||||
],
|
||||
null: false
|
||||
|
||||
add :authorization_code_ttl, :integer, null: false
|
||||
add :access_token_ttl, :integer, null: false
|
||||
add :pkce, :boolean, default: false, null: false
|
||||
add :public_key, :text
|
||||
add :private_key, :text, null: false
|
||||
add :id_token_ttl, :integer, default: 3600
|
||||
add :public_refresh_token, :boolean, null: false, default: false
|
||||
add :refresh_token_ttl, :integer, null: false, default: "2592000"
|
||||
add :public_revoke, :boolean, null: false, default: false
|
||||
add :id_token_signature_alg, :string, default: "RS512"
|
||||
add :confidential, :boolean, default: false, null: false
|
||||
add :jwt_public_key, :text
|
||||
add :token_endpoint_auth_methods, {:array, :string}, null: false,
|
||||
default: ["client_secret_basic", "client_secret_post"]
|
||||
add :token_endpoint_jwt_auth_alg, :string, default: "HS256", null: false
|
||||
add :userinfo_signed_response_alg, :string
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:oauth_tokens, primary_key: false) do
|
||||
add :id, :uuid, primary_key: true
|
||||
add :type, :string
|
||||
add :value, :string
|
||||
add :refresh_token, :string
|
||||
add :expires_at, :integer
|
||||
add :redirect_uri, :string
|
||||
add :state, :string
|
||||
add :scope, :string, default: ""
|
||||
add :revoked_at, :utc_datetime_usec
|
||||
add :code_challenge_hash, :string
|
||||
add :code_challenge_method, :string
|
||||
add :nonce, :string
|
||||
add :previous_token, :string
|
||||
add :refresh_token_revoked_at, :utc_datetime_usec
|
||||
add :previous_code, :string
|
||||
|
||||
add :client_id, references(:oauth_clients, type: :uuid, on_delete: :nilify_all)
|
||||
add :sub, :string
|
||||
|
||||
timestamps(type: :utc_datetime_usec)
|
||||
end
|
||||
|
||||
create table(:oauth_scopes, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :label, :string
|
||||
add :name, :string, default: ""
|
||||
add :public, :boolean, default: false, null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:oauth_clients_scopes) do
|
||||
add :client_id, references(:oauth_clients, type: :uuid, on_delete: :delete_all)
|
||||
add :scope_id, references(:oauth_scopes, type: :uuid, on_delete: :delete_all)
|
||||
end
|
||||
|
||||
create unique_index(:oauth_clients, [:id, :secret])
|
||||
create index(:oauth_tokens, [:value])
|
||||
create unique_index(:oauth_tokens, [:client_id, :value])
|
||||
create unique_index(:oauth_tokens, [:client_id, :refresh_token])
|
||||
create unique_index(:oauth_scopes, [:name])
|
||||
end
|
||||
end
|
||||
9
priv/repo/migrations/20231106235646_user_last_login.exs
Normal file
9
priv/repo/migrations/20231106235646_user_last_login.exs
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule SsoBsn.Repo.Migrations.UserLastLogin do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :last_login_at, :utc_datetime_usec
|
||||
end
|
||||
end
|
||||
end
|
||||
20
shell.nix
20
shell.nix
@@ -1,6 +1,20 @@
|
||||
{ mkShell, elixir, elixir-ls, inotify-tools, sqlite-interactive }:
|
||||
mkShell {
|
||||
packages = [ elixir elixir-ls inotify-tools sqlite-interactive ];
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
mkShell,
|
||||
elixir,
|
||||
elixir-ls,
|
||||
inotify-tools,
|
||||
sqlite-interactive,
|
||||
}:
|
||||
mkShell {
|
||||
packages =
|
||||
[elixir elixir-ls sqlite-interactive]
|
||||
++ lib.lists.optional (pkgs.system == "x86_64-linux") inotify-tools
|
||||
++ lib.lists.optionals (pkgs.system == "aarch64-darwin") (with pkgs.darwin.apple_sdk.frameworks; [
|
||||
CoreFoundation
|
||||
CoreServices
|
||||
]);
|
||||
shellHook = ''
|
||||
mkdir -p .nix-mix
|
||||
mkdir -p .nix-hex
|
||||
|
||||
Reference in New Issue
Block a user