diff --git a/.gitea/workflows/elixir.yml b/.gitea/workflows/elixir.yml new file mode 100644 index 0000000..f6f14a5 --- /dev/null +++ b/.gitea/workflows/elixir.yml @@ -0,0 +1,32 @@ +name: Base CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 + with: + elixir-version: '1.14.0' # [Required] Define the Elixir version + otp-version: '25.0' # [Required] Define the Erlang/OTP version + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install dependencies + run: mix deps.get + - name: Run tests + run: mix test diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml new file mode 100644 index 0000000..f6f14a5 --- /dev/null +++ b/.github/workflows/elixir.yml @@ -0,0 +1,32 @@ +name: Base CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 + with: + elixir-version: '1.14.0' # [Required] Define the Elixir version + otp-version: '25.0' # [Required] Define the Erlang/OTP version + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install dependencies + run: mix deps.get + - name: Run tests + run: mix test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..86a113e --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +up: + sudo docker compose up -d + make migrate-up + mix phx.server + +migrate-up: + mix ecto.migrate + +migrate-down: + mix ecto.rollback + diff --git a/README.md b/README.md index 0b50ae0..b420bcb 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). +### Dev DB creds +From *./config/dev.exs* +Host: localhost:5432 +Database: draincloud_core_dev +Username: postgres +Password: postgres + ### Some phoenix related links * Official website: https://www.phoenixframework.org/ diff --git a/docker-compose.yaml b/docker-compose.yaml index 5f06def..d2bf9b9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,24 +9,24 @@ services: ports: - 5432:5432 - minio: - image: quay.io/minio/minio:RELEASE.2024-08-29T01-40-52Z - command: server --console-address ":9001" http://minio/data{1...2} - hostname: minio - volumes: - - data-1:/data1 - - data-2:/data2 - expose: - - "9000" - - "9001" - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 5s - timeout: 5s - retries: 5 + # minio: + # image: quay.io/minio/minio:RELEASE.2024-08-29T01-40-52Z + # command: server --console-address ":9001" http://minio/data{1...2} + # hostname: minio + # volumes: + # - data-1:/data1 + # - data-2:/data2 + # expose: + # - "9000" + # - "9001" + # environment: + # MINIO_ROOT_USER: minioadmin + # MINIO_ROOT_PASSWORD: minioadmin + # healthcheck: + # test: ["CMD", "mc", "ready", "local"] + # interval: 5s + # timeout: 5s + # retries: 5 volumes: draincore-data: diff --git a/lib/draincloud_core/auth/auth.ex b/lib/draincloud_core/auth/auth.ex index abf9db2..f963afa 100644 --- a/lib/draincloud_core/auth/auth.ex +++ b/lib/draincloud_core/auth/auth.ex @@ -1,6 +1,10 @@ defmodule DraincloudCore.Auth do def is_logon(_token) do # TODO here - true + false + end + + def auth_cookies(conn = %Plug.Conn{}, user = %DrainCloudCore.Auth.Users{}) do + end end diff --git a/lib/draincloud_core/auth/sessions.ex b/lib/draincloud_core/auth/sessions.ex new file mode 100644 index 0000000..827f4ae --- /dev/null +++ b/lib/draincloud_core/auth/sessions.ex @@ -0,0 +1,21 @@ +defmodule DrainCloudCore.Auth.Session do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + + schema "sessions" do + field :token, :string + field :user_id, :id + field :user_agent, :string + field :created_at, :utc_datetime + field :expires_at, :utc_datetime + end + + def changeset(session, params \\ %{}) do + session + |> cast(params, [:id, :token, :user_id, :user_agent, :created_at, :expires_at]) + |> validate_required([:id, :token, :user_id, :user_agent, :created_at, :expires_at]) + end + + +end diff --git a/lib/draincloud_core/auth/sessions_store.ex b/lib/draincloud_core/auth/sessions_store.ex new file mode 100644 index 0000000..cdc89ad --- /dev/null +++ b/lib/draincloud_core/auth/sessions_store.ex @@ -0,0 +1,45 @@ +defmodule DrainCloudCore.Auth.SessionsStore do + @behaviour Plug.Session.Store + + @token_len 64 + + alias DrainCloudCore.Auth.SessionsRepo + alias DrainCloudCore.Repo, as: Repo + + def init(_opts), do: :ok + + def put(conn, sid, any, opts) do + + end + + def get(conn, cookie, opts) do + + end + + def delete(conn, sid, opts) do + + end + + def new_session(conn, user) do + %DrainCloudCore.Auth.Session { + user_id: user.id, + token: new_token(@token_len), + user_agent: user_agent(conn), + created_at: DateTime.utc_now(), + expires_at: DateTime.add(DateTime.utc_now(), 7*24, :hour) + } + end + + defp user_agent(conn) do + Enum.find_value(conn.req_headers, "", fn x -> + case x do + {"user-agent", agent} -> agent + _ -> nil + end + end) + end + + defp new_token(length) do + :crypto.strong_rand_bytes(length) |> Base.url_encode64 |> binary_part(0, length) + end +end diff --git a/lib/draincloud_core/auth/users.ex b/lib/draincloud_core/auth/users.ex new file mode 100644 index 0000000..5e243cb --- /dev/null +++ b/lib/draincloud_core/auth/users.ex @@ -0,0 +1,29 @@ +defmodule DrainCloudCore.Auth.Users do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + + alias DrainCloudCore.Repo, as: Repo + + schema "users" do + field :login, :string + field :password, :string, redact: true + field :created_at, :utc_datetime + field :updated_at, :utc_datetime + field :deleted_at, :utc_datetime + end + + def changeset(user, params \\ %{}) do + user + |> cast(params, [:id, :login, :password, :updated_at, :deleted_at]) + |> validate_required([:id]) + end + + def add_user(user) do + Repo.insert(user) + end + + def password_by_login(login) do + Repo.all(from u in "users", where: u.login == ^login, select: u.password, limit: 1) + end +end diff --git a/lib/draincloud_core/repo.ex b/lib/draincloud_core/repo/repo.ex similarity index 100% rename from lib/draincloud_core/repo.ex rename to lib/draincloud_core/repo/repo.ex diff --git a/lib/draincloud_core/rtc.ex b/lib/draincloud_core/rtc.ex index 22e62f5..21aa64a 100644 --- a/lib/draincloud_core/rtc.ex +++ b/lib/draincloud_core/rtc.ex @@ -10,6 +10,7 @@ defmodule DrainCloudCore.Rtc.Application do type: :supervisor } end + def start_link(_) do children = [ DrainCloudCore.Rtc @@ -27,27 +28,32 @@ defmodule DrainCloudCore.Rtc do use GenServer # Real-time config table schemas - @config_web [attributes: [ - :host, - :enable_https, - :port, - ]] + @config_web [ + attributes: [ + :host, + :enable_https, + :port + ] + ] - @config_pg [attributes: [ - :host, - :port, - :ssl, - :user, - :password, - :db, - ]] + @config_pg [ + attributes: [ + :host, + :port, + :ssl, + :user, + :password, + :db + ] + ] - - @config_s3 [attributes: [ - :host, - :port, - # user / secret / secrets etc... - ]] + @config_s3 [ + attributes: [ + :host, + :port + # user / secret / secrets etc... + ] + ] # def child_spec(opts) do # %{ @@ -81,6 +87,7 @@ defmodule DrainCloudCore.Rtc do def fetch_config() do # Mnesia.read() end + def init() do Log.info("[#{__MODULE__}] start initializin RTC subsystems") diff --git a/lib/draincloud_core_web/components/layouts/root.html.heex b/lib/draincloud_core_web/components/layouts/root.html.heex index 90e81b6..f4782bc 100644 --- a/lib/draincloud_core_web/components/layouts/root.html.heex +++ b/lib/draincloud_core_web/components/layouts/root.html.heex @@ -8,8 +8,13 @@ <%= assigns[:page_title] || "DrainCloudCore" %> - - + diff --git a/lib/draincloud_core_web/controllers/auth_controller/auth_controller.ex b/lib/draincloud_core_web/controllers/auth_controller/auth_controller.ex index e8e681b..77a6331 100644 --- a/lib/draincloud_core_web/controllers/auth_controller/auth_controller.ex +++ b/lib/draincloud_core_web/controllers/auth_controller/auth_controller.ex @@ -1,12 +1,45 @@ defmodule DrainCloudCoreWeb.AuthController do use DrainCloudCoreWeb, :controller - alias DrainCloudCoreWeb.Request, as: Request - def logon(conn, _params) do - if Request.hs_token?(conn) do - # TODO validate token here + import Plug.Conn + + alias DrainCloudCoreWeb.AuthController.RegisterRequest, as: RegisterRequest + alias DrainCloudCoreWeb.AuthController.LoginRequest, as: LoginRequest + alias :logger, as: Log + alias DrainCloudCore.Auth.Users , as: Repo + + def register(conn, _params) do + Log.debug( + Enum.find_value(conn.req_headers, "", fn x -> + case x do + {"user-agent", agent} -> agent + _ -> nil + end + end) + |> inspect + ) + + try do + RegisterRequest.from_request(conn) + |> RegisterRequest.to_model + |> Repo.add_user + send_resp(conn, 200, Jason.encode! %{ok: true}) + rescue + e in RuntimeError -> + Log.error("failed to create new user: #{e}") + raise e end + end - + def login(conn, _params) do + try do + LoginRequest.from_request(conn) + # TODO send cookies and tokens + send_resp(conn, 200, Jason.encode! %{ok: true}) + rescue + e in RuntimeError -> + Log.error("failed to insert new user: #{e}") + raise e + end end end diff --git a/lib/draincloud_core_web/controllers/auth_controller/login_request.ex b/lib/draincloud_core_web/controllers/auth_controller/login_request.ex new file mode 100644 index 0000000..c1368d0 --- /dev/null +++ b/lib/draincloud_core_web/controllers/auth_controller/login_request.ex @@ -0,0 +1,68 @@ +defmodule DraincloudCoreWeb.AuthController.LoginRequest do + @derive [Poison.Encoder, Jason.Encoder] + + defstruct login: "", password: "" + + alias __MODULE__, as: Request + alias DrainCloudCore.Auth.Users, as: User + alias DrainCloudCoreWeb.Errors.InvalidArgumentException, as: InvalidArgumentException + + def from_request(conn = %Plug.Conn{}) do + validate_and_build(conn.params) + end + + def to_model(req = %Request{}) do + %User{ + login: req.login, + password: req.password + } + end + + defp validate_and_build(req) do + validate(req) + |> build + end + + defp validate(req) do + validate_login(req) + |> validate_password + end + + defp validate_login(req) do + if String.length(Map.get(req, "login")) >= 3 do + req + else + raise InvalidArgumentException, %{ + message: "login must be 3 symbols at minimum", + args: ["login"] + } + end + end + + defp validate_password(req) do + if String.length(Map.get(req, "password")) >= 8 do + req + else + raise InvalidArgumentException, %{ + message: "password must be 8 symbols at minimum", + args: ["password"] + } + end + end + + defp build(req) do + %Request{ + login: Map.get(req, "login"), + password: Map.get(req, "password") # TODO hash password + } + end + + defimpl String.Chars, for: __MODULE__ do + def to_string(req) do + case Jason.encode(req) do + {:ok, str} -> str + {:error, msg} -> raise "failed to encode login request to string: #{msg}" + end + end + end +end diff --git a/lib/draincloud_core_web/controllers/auth_controller/register_request.ex b/lib/draincloud_core_web/controllers/auth_controller/register_request.ex new file mode 100644 index 0000000..ea8d168 --- /dev/null +++ b/lib/draincloud_core_web/controllers/auth_controller/register_request.ex @@ -0,0 +1,71 @@ +defmodule DrainCloudCoreWeb.AuthController.RegisterRequest do + @derive [Poison.Encoder, Jason.Encoder] + + defstruct login: "", password: "" + + alias __MODULE__, as: Request + alias DrainCloudCore.Auth.Users, as: User + alias DrainCloudCoreWeb.Errors.InvalidArgumentException, as: InvalidArgumentException + + def from_request(conn = %Plug.Conn{}) do + # TODO remove Kernel.inspect calls + :logger.debug("[from_request] incoming request: #{Kernel.inspect(conn.params)}") + + validate_and_build(conn.params) + end + + def to_model(req = %Request{}) do + %User{ + login: req.login, + password: req.password + } + end + + defp validate_and_build(req) do + validate(req) + |> build + end + + defp validate(req) do + validate_login(req) + |> validate_password + end + + defp validate_login(req) do + if String.length(Map.get(req, "login")) >= 3 do + req + else + raise InvalidArgumentException, %{ + message: "login must be 3 symbols at minimum", + args: ["login"] + } + end + end + + defp validate_password(req) do + if String.length(Map.get(req, "password")) >= 4 do + req + else + raise InvalidArgumentException, %{ + message: "password must be 4 symbols at minimum", + args: ["password"] + } + end + end + + defp build(req) do + %Request{ + login: Map.get(req, "login"), + password: Map.get(req, "password") # TODO hash password + } + end + + defimpl String.Chars, for: __MODULE__ do + def to_string(req) do + case Jason.encode(req) do + {:ok, str} -> str + {:error, msg} -> raise "failed to encode register request to string: #{msg}" + end + end + end +end diff --git a/lib/draincloud_core_web/controllers/errors/errors.ex b/lib/draincloud_core_web/controllers/errors/errors.ex new file mode 100644 index 0000000..626a7a3 --- /dev/null +++ b/lib/draincloud_core_web/controllers/errors/errors.ex @@ -0,0 +1,28 @@ +defmodule DrainCloudCoreWeb.Errors.InvalidArgumentException do + @derive [Jason.Encoder] + + alias __MODULE__, as: InvalidArgumentException + defexception [:message, :args] + + @impl true + def exception(%{message: message, args: args}) do + %InvalidArgumentException{message: message, args: args} + end + + @impl true + def exception(term) do + case term do + [] -> %InvalidArgumentException{} + _ -> %InvalidArgumentException{message: "Error: " <> term} + end + end + + defimpl String.Chars, for: __MODULE__ do + def to_string(err) do + case Jason.encode(err) do + {:ok, str} -> str + {:error, msg} -> raise "failed to encode register request to string: #{msg}" + end + end + end +end diff --git a/lib/draincloud_core_web/controllers/main_controller/main_controller.ex b/lib/draincloud_core_web/controllers/main_controller/main_controller.ex index 7a93c37..6e2f0da 100644 --- a/lib/draincloud_core_web/controllers/main_controller/main_controller.ex +++ b/lib/draincloud_core_web/controllers/main_controller/main_controller.ex @@ -7,5 +7,4 @@ defmodule DrainCloudCoreWeb.MainController do |> put_root_layout(false) |> json(%{data: %{name: "Some Name"}}) end - end diff --git a/lib/draincloud_core_web/endpoint.ex b/lib/draincloud_core_web/endpoint.ex index 2348684..4aa1f25 100644 --- a/lib/draincloud_core_web/endpoint.ex +++ b/lib/draincloud_core_web/endpoint.ex @@ -15,10 +15,6 @@ defmodule DrainCloudCoreWeb.Endpoint do websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phx.digest - # when deploying your static files in production. plug Plug.Static, at: "/", from: :draincloud_core, diff --git a/lib/draincloud_core_web/error_handler.ex b/lib/draincloud_core_web/error_handler.ex new file mode 100644 index 0000000..f47ee71 --- /dev/null +++ b/lib/draincloud_core_web/error_handler.ex @@ -0,0 +1,10 @@ +defmodule DrainCloudCoreWeb.ErrorHandler do + alias DrainCloudCoreWeb.Errors.InvalidArgumentException, as: InvalidArgumentException + def handle_reason(reason = %InvalidArgumentException{}), do: Jason.encode!(reason) + + def handle_reason(reason) do + :logger.error(inspect(reason)) + + Jason.encode!(%{error: "Oops! Something went wrong!"}) + end +end diff --git a/lib/draincloud_core_web/gettext.ex b/lib/draincloud_core_web/gettext.ex index 5a13399..49a9038 100644 --- a/lib/draincloud_core_web/gettext.ex +++ b/lib/draincloud_core_web/gettext.ex @@ -1,24 +1,3 @@ defmodule DrainCloudCoreWeb.Gettext do - @moduledoc """ - A module providing Internationalization with a gettext-based API. - - By using [Gettext](https://hexdocs.pm/gettext), - your module gains a set of macros for translations, for example: - - import DrainCloudCoreWeb.Gettext - - # Simple translation - gettext("Here is the string to translate") - - # Plural translation - ngettext("Here is the string to translate", - "Here are the strings to translate", - 3) - - # Domain-based translation - dgettext("errors", "Here is the error message to translate") - - See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. - """ use Gettext, otp_app: :draincloud_core end diff --git a/lib/draincloud_core_web/router.ex b/lib/draincloud_core_web/router.ex index 9ed3551..f141bf2 100644 --- a/lib/draincloud_core_web/router.ex +++ b/lib/draincloud_core_web/router.ex @@ -1,5 +1,9 @@ defmodule DrainCloudCoreWeb.Router do use DrainCloudCoreWeb, :router + use Plug.ErrorHandler + import Plug.Conn + + alias DrainCloudCoreWeb.ErrorHandler, as: ErrorHandler pipeline :browser do plug :accepts, ["html"] @@ -14,24 +18,19 @@ defmodule DrainCloudCoreWeb.Router do plug :accepts, ["json"] end - scope "/", DrainCloudCoreWeb do + scope "/api", DrainCloudCoreWeb do pipe_through :api - get "/api", MainController, :test + post "/register", AuthController, :register end - # Other scopes may use custom stacks. - # scope "/api", DrainCloudCoreWeb do - # pipe_through :api - # end + @impl Plug.ErrorHandler + def handle_errors(conn, %{kind: _kind, reason: reason, stack: _stack}) do + + send_resp(conn, 500, ErrorHandler.handle_reason(reason)) + end - # Enable LiveDashboard in development if Application.compile_env(:draincloud_core, :dev_routes) do - # If you want to use the LiveDashboard in production, you should put - # it behind authentication and allow only admins to access it. - # If your application does not have an admins-only section yet, - # you can use Plug.BasicAuth to set up some basic authentication - # as long as you are also using SSL (which you should anyway). import Phoenix.LiveDashboard.Router scope "/dev" do diff --git a/mix.exs b/mix.exs index 54fa208..48c6cd5 100644 --- a/mix.exs +++ b/mix.exs @@ -44,6 +44,7 @@ defmodule DrainCloudCore.MixProject do {:phoenix_live_dashboard, "~> 0.8.3"}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, + {:poison, "~> 6.0"}, {:heroicons, github: "tailwindlabs/heroicons", tag: "v2.1.1", diff --git a/mix.lock b/mix.lock index 04c3e61..13d2b03 100644 --- a/mix.lock +++ b/mix.lock @@ -27,6 +27,7 @@ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [: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", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"}, "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [: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", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, diff --git a/priv/repo/migrations/20240901221317_users.exs b/priv/repo/migrations/20240901221317_users.exs deleted file mode 100644 index 8395c5a..0000000 --- a/priv/repo/migrations/20240901221317_users.exs +++ /dev/null @@ -1,7 +0,0 @@ -defmodule DrainCloudCore.Repo.Migrations.Users do - use Ecto.Migration - - def change do - # TODO users table migration - end -end diff --git a/priv/repo/migrations/20240908091209_add_users.exs b/priv/repo/migrations/20240908091209_add_users.exs new file mode 100644 index 0000000..cd4a94b --- /dev/null +++ b/priv/repo/migrations/20240908091209_add_users.exs @@ -0,0 +1,15 @@ +defmodule DrainCloudCore.Repo.Migrations.AddUsers do + use Ecto.Migration + + def change do + create table(:users) do + add :login, :string, size: 120 + add :password, :string + add :created_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP") + add :updated_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP") + add :deleted_at, :utc_datetime, null: true, default: nil + end + + create unique_index(:users, :login) + end +end diff --git a/priv/repo/migrations/20240917204624_sessions.exs b/priv/repo/migrations/20240917204624_sessions.exs new file mode 100644 index 0000000..626af6a --- /dev/null +++ b/priv/repo/migrations/20240917204624_sessions.exs @@ -0,0 +1,15 @@ +defmodule DrainCloudCore.Repo.Migrations.Sessions do + use Ecto.Migration + + def change do + create table(:sessions) do + add :token, :string + add :user_id, references("users", on_delete: :nothing) + add :user_agent, :string + add :created_at, :utc_datetime + add :expires_at, :utc_datetime + end + + create unique_index(:sessions, :token) + end +end diff --git a/schemas/users.sql b/schemas/users.sql deleted file mode 100644 index c2ae31e..0000000 --- a/schemas/users.sql +++ /dev/null @@ -1,5 +0,0 @@ -create table users ( - id bigserial, - login varchar(50), - password varchar(256) -); \ No newline at end of file diff --git a/test/draincloud_core_web/controllers/error_html_test.exs b/test/draincloud_core_web/controllers/error_html_test.exs index 73599fb..d24cdc6 100644 --- a/test/draincloud_core_web/controllers/error_html_test.exs +++ b/test/draincloud_core_web/controllers/error_html_test.exs @@ -9,6 +9,7 @@ defmodule DrainCloudCoreWeb.ErrorHTMLTest do end test "renders 500.html" do - assert render_to_string(DrainCloudCoreWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + assert render_to_string(DrainCloudCoreWeb.ErrorHTML, "500", "html", []) == + "Internal Server Error" end end diff --git a/test/draincloud_core_web/controllers/error_json_test.exs b/test/draincloud_core_web/controllers/error_json_test.exs index 20aa6b0..a44ef0b 100644 --- a/test/draincloud_core_web/controllers/error_json_test.exs +++ b/test/draincloud_core_web/controllers/error_json_test.exs @@ -2,7 +2,9 @@ defmodule DrainCloudCoreWeb.ErrorJSONTest do use DrainCloudCoreWeb.ConnCase, async: true test "renders 404" do - assert DrainCloudCoreWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + assert DrainCloudCoreWeb.ErrorJSON.render("404.json", %{}) == %{ + errors: %{detail: "Not Found"} + } end test "renders 500" do