Serve sigit app auth
Seto Elkahfi committed
Jun 17, 2026 at 12:25 UTC
06658f2d8c63390d5b1eff061b1a080f681992d4
8 files changed
+296
-84
.agents/skills/deployment/SKILL.md
+172
new file mode 100644
index 0000000..20715fb
--- /dev/null
+++ b/.agents/skills/deployment/SKILL.md
@@ -0,0 +1,172 @@
+---
+name: deployment
+description: How to deploy, migrate, seed, and restart the sigit.si Rails app in production. Use this whenever shipping sigit-si, debugging a 500 after deploy, running a migration or seed on prod, or restarting Puma.
+---
+
+# Deploying sigit.si
+
+The Rails app behind `https://sigit.si` (the code + model hosting platform). Deploys happen by pushing `main` to a bare repo whose `post-receive` hook checks out the work tree and restarts Puma.
+
+## Server facts
+
+| Detail | Value |
+| ------ | ----- |
+| Domain | `sigit.si` (also the landing page `getsiti.5mb.app`) |
+| Host | `api.splitfire.ai` = `65.21.240.91` (Hetzner Debian, `debian-4gb-hel1-2`) |
+| SSH | `ssh smb1-deploy` (user `deploy`); the app runs as user `git` |
+| App dir | `/home/git/apps/sigitsi` (deployed in place, not Capistrano releases) |
+| Bare repo | `/home/git/sigitsi.git` (push target; `post-receive` hook deploys) |
+| Puma port | 3015 (`SIGITSI_PORT`), single mode |
+| Ruby | 3.4.2 via rbenv at `/home/git/.rbenv` |
+| User repos | `/home/git/repos/users/<username>/<repo>.git` (`SIGITSI_REPOS_PATH`) |
+| nginx vhost | `/etc/nginx/sites-enabled/getsiti.5mb.app` (proxies to 3015) |
+
+Note: this is a **different box** from `api.smbcloud.xyz` (`deploy-sigitweb`), which only hosts the static landing site. Do not look for the Rails app there.
+
+### Acting as the app user
+
+The app, its files, and its Postgres role all belong to `git`. The `deploy` user has passwordless `sudo -u git`. A `git` login shell already has `RAILS_ENV=production`, the DB password (`SIGITSI_DATABASE_PASSWORD`), rbenv, and bundle on PATH, so run app commands like this:
+
+```bash
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && <command>"'
+```
+
+There is no `.env` file; env vars come from `git`'s login shell (`~/.profile`).
+
+## Deploying
+
+From a local clone with the `main` branch ready:
+
+```bash
+git push <prod-remote> main
+```
+
+The `post-receive` hook (`/home/git/sigitsi.git/hooks/post-receive`) then:
+
+1. `git checkout -f main` into `/home/git/apps/sigitsi`
+2. `rbenv local`, `nvm use`, `bundle install`
+3. `rake assets:precompile`
+4. `rake db:prepare`
+5. `kill -9 $(lsof -t -i:3015)` then `bundle exec puma -e production > output.log 2>&1 &`
+6. `bin/jobs start > output-jobs.log 2>&1 &`
+
+### The hook does not reliably migrate (read this before trusting a deploy)
+
+The hook has **no `set -e`**, so a failed DB step does not stop the deploy: Puma restarts anyway, on whatever schema is live. Worse, `rake db:prepare` and `rake db:migrate` operate on **all four configured databases** (primary + solid `cache`/`queue`/`cable`). The cache/queue/cable databases do **not** exist in production and the `sigitsi` role lacks `CREATEDB`, so the task aborts with `permission denied to create database` before it ever migrates the primary DB. Its output goes to the pushing client, not to `output.log` (which Puma immediately truncates), so the failure is invisible afterward.
+
+Result: a deploy that adds a migration leaves the schema stale and every route touching the new column returns 500, while the home page and auth pages (which do not touch it) stay up. See "Run a migration" below for the fix, and "Known issues".
+
+## Run a migration
+
+The deploy hook's `rake db:prepare` now self-migrates (all four databases exist; see "Postgres admin"). To migrate by hand:
+
+```bash
+# check what is pending first
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail"'
+
+# apply
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate"'
+```
+
+If the cache/queue/cable databases are ever missing again, `db:migrate` aborts trying to create them (the `sigitsi` role lacks `CREATEDB`). Scope to the primary to get past it: `bin/rails db:migrate:primary`.
+
+Then restart Puma (next section) so ActiveRecord regenerates attribute methods for the new columns. A live process with a stale schema raises `NameError: undefined method 'kind'` even after the column exists.
+
+## Seed production data
+
+The model library ships with demo content (the `bartowski/Qwen2.5-3B-Instruct-GGUF` model and friends). Production needs this seeded once, e.g. so `https://sigit.si/bartowski/Qwen2.5-3B-Instruct-GGUF` resolves.
+
+With all four databases present, `bin/rails db:seed` works directly. If the cache/queue/cable DBs are missing, `db:seed`'s `db:abort_if_pending_migrations` prerequisite aborts on them; bypass it by loading the seed straight against the primary connection:
+
+```bash
+# normal
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:seed"'
+# fallback if the solid DBs are missing
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails runner \"Rails.application.load_seed\""'
+```
+
+What it creates (`db/seeds.rb`, idempotent — safe to re-run):
+
+- Demo owner users (`bartowski`, `meta-llama`, `sentence-transformers`) and one model under the existing `sigit` user (`SiGit-Coder-1.5B-GGUF`).
+- Each model's bare git repo under `/home/git/repos/users/<owner>/<name>.git`, holding a YAML model card and Git LFS pointer files for the weights.
+
+The box has `git-lfs` installed, so the seed deliberately neutralises the local LFS filters (`filter.lfs.clean=cat`, `filter.lfs.process=`) before committing the pointer text. Without that, git-lfs tries to upload real objects and the push fails. This is already handled in `db/seeds.rb`.
+
+Verify:
+
+```bash
+curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/bartowski/Qwen2.5-3B-Instruct-GGUF
+```
+
+## Restart Puma
+
+Single-mode Puma on 3015. Two options:
+
+```bash
+# Preferred: hot restart (re-exec, ~zero downtime). Get the master pid:
+ssh smb1-deploy 'ss -ltnp | grep 3015' # or: sudo -u git lsof -t -i:3015
+ssh smb1-deploy 'sudo -u git kill -USR2 <master_pid>'
+
+# Full restart (matches the deploy hook), if a hot restart misbehaves:
+ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && kill -9 \$(lsof -t -i:3015); sleep 1; setsid bundle exec puma -e production > output.log 2>&1 < /dev/null &"'
+```
+
+Confirm it rebound and serves:
+
+```bash
+ssh smb1-deploy 'ss -ltnp | grep 3015'
+ssh smb1-deploy 'curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" http://127.0.0.1:3015/models'
+```
+
+There is no systemd unit; Puma is a detached background process owned by `git`.
+
+## nginx
+
+The vhost is `/etc/nginx/sites-enabled/sigit.si` (resolve symlinks before editing). It terminates TLS, serves static files from `/home/git/apps/sigitsi/public`, and proxies everything else to the `puma15` upstream (`127.0.0.1:3015`).
+
+### The static-extension location must fall through to the app
+
+sigit.si serves **repo file contents** at arbitrary paths (`/:user/:repo/blob/:branch/*path` and `/raw/...`), so URLs like `/sigit/nord/blob/main/public/web-app-manifest-192x192.png` are app routes, not files on disk. The vhost has a catch-all static block:
+
+```nginx
+location ~ ^(?!/rails/).+\.(jpg|jpeg|gif|png|ico|json|txt|xml)$ {
+ ...
+ try_files $uri @puma15; # NOT =404
+}
+```
+
+It **must** end in `try_files $uri @puma15;`. With the stock `try_files $uri =404;`, nginx looks for the file under `public/`, does not find it, and returns 404 without ever reaching Rails. The result: any blob/raw URL whose path ends in `.png/.jpg/.json/.ico/.txt/.xml` (etc.) 404s, while `.md` and other extensions work. The shared `server-nginx-rails` template ships the `=404` form, which is fine for normal apps but wrong here.
+
+### Editing the vhost safely
+
+- `nginx` includes every file in `sites-enabled/`. Never leave a backup (`sigit.si.bak.*`) there or `nginx -t` fails with `duplicate upstream "puma15"`. Keep backups in `/root` or `/tmp`.
+- Always `sudo nginx -t` before `sudo systemctl reload nginx`.
+
+## Debugging a 500 after deploy
+
+1. Read the app log (Puma stdout): `sudo -u git tail -80 /home/git/apps/sigitsi/output.log`. Look for `PG::UndefinedColumn` (pending migration) or `NameError` on a new attribute (stale schema, needs a Puma restart).
+2. `bin/rails db:migrate:status` to see if a migration is `down`.
+3. Note that `allow_browser versions: :modern` returns **403** to clients with no/old User-Agent, so always pass a modern UA when curling, or a healthy route looks broken.
+
+## Postgres admin
+
+PostgreSQL 13, local. The app role `sigitsi` connects over the local socket with a password and has **no** `CREATEDB` or superuser. For any admin task (creating databases, granting roles) go through the `postgres` superuser, which authenticates by `peer`:
+
+```bash
+ssh smb1-deploy 'sudo -u postgres psql -c "<SQL>"'
+```
+
+The four databases (`sigitsi_production` plus the solid `_cache` / `_queue` / `_cable`) now exist, each owned by `sigitsi`, created with:
+
+```sql
+CREATE DATABASE sigitsi_production_cache OWNER sigitsi;
+CREATE DATABASE sigitsi_production_queue OWNER sigitsi;
+CREATE DATABASE sigitsi_production_cable OWNER sigitsi;
+```
+
+Owning the database lets `sigitsi` create its own tables there, so no extra `GRANT` is needed. Creating them (rather than `ALTER ROLE sigitsi CREATEDB`) keeps the app role least-privileged. The `_cache`/`_queue`/`_cable` databases stay empty by design (see below).
+
+## Known issues
+
+- **The solid databases are unused.** Production sets Action Cable to the `redis` adapter (`cable.yml`), never sets `config.cache_store = :solid_cache_store`, and does not run solid_queue (no `queue_adapter`, `bin/jobs` is missing so the hook's `bin/jobs start` no-ops). So `_cache`/`_queue`/`_cable` exist only to satisfy the multi-database scaffold in `config/database.yml` and stay empty. They were created so `rake db:prepare` stops aborting and deploys self-migrate. The cleaner long-term fix is to delete the `cache`/`queue`/`cable` blocks from `config/database.yml` (all environments) so Rails manages only the primary; that removes the empty databases and the need for them to exist at all.
+- **Deploy hook is not fail-safe.** No `set -e`, and Puma's stdout truncates the same `output.log` that captured the deploy steps, so migration failures ship silently. Hardening it (fail on a bad `db:prepare`, log deploy output to a separate file) is worthwhile.
app/controllers/api/base_controller.rb
+36
-57
index f363c5e..a81ea4d 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -3,42 +3,42 @@
module Api
# Base for all JSON API controllers.
#
- # Unlike the HTML controllers, the API is token-based: clients (e.g. the
- # siGit Code & Deploy desktop app) send the smbCloud access token as
- # `Authorization: Bearer <token>` instead of relying on the Rails session
- # cookie. Response shapes mirror the desktop client's AccountStatus contract:
+ # The API is token-based: clients (the siGit Code & Deploy desktop app) send
+ # the smbCloud access token as `Authorization: Bearer <token>`. Response bodies
+ # match the desktop's shared-model Rust types (smbcloud-model) exactly, so the
+ # app deserializes them directly:
#
- # ready → { status: "ready", access_token:, user: {...} }
- # not_found → { status: "not_found", error_code:, message: }
- # incomplete → { status: "incomplete", error_code:, message: }
- # error → { status: "error", error_code:, message: }
+ # AccountStatus → "NotFound" | {"Ready":{"access_token":…}} | {"Incomplete":{"status":<u32>}}
+ # User → {"id":,"email":,"created_at":,"updated_at":}
+ # SignupResult → {"code":,"message":,"data":{…}}
+ # ErrorResponse → {"error_code":<i32>,"message":}
class BaseController < ActionController::API
- # Order matters: rescue_from is matched in reverse declaration order, so the
- # base AuthenticationError is declared first and its subclasses after, which
- # makes the more specific handlers win.
- rescue_from SmbcloudAuthService::AuthenticationError, with: :render_auth_error
- rescue_from SmbcloudAuthService::AccountNotFoundError, with: :render_not_found
- rescue_from SmbcloudAuthService::AccountIncompleteError, with: :render_incomplete
- rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
- rescue_from KeyError, with: :render_config_error
+ # error_codes::ErrorCode (i32) — used in ErrorResponse.
+ ERR_UNKNOWN = 0
+ ERR_UNAUTHORIZED = 100
+ ERR_INVALID = 101
+ # account::ErrorCode (u32) — used in AccountStatus::Incomplete.status.
+ ACCOUNT_ERROR_CODES = [ 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007 ].freeze
+ ACCOUNT_EMAIL_UNVERIFIED = 1001
+
+ rescue_from SmbcloudAuthService::AuthenticationError, with: :render_auth_error
+ rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
+ rescue_from KeyError, with: :render_config_error
private
- # Verifies the bearer token against smbCloud and upserts the local user.
- # On success sets @access_token and @current_user; otherwise halts with 401.
+ # Verifies the bearer token against smbCloud, exposes the profile in
+ # @me_profile, and upserts the local mirror. On failure halts with an
+ # ErrorResponse (401).
def authenticate_token!
token = bearer_token
- if token.blank?
- return render_error("Missing access token.", status: :unauthorized)
- end
+ return render_error(ERR_UNAUTHORIZED, "Missing access token.", status: :unauthorized) if token.blank?
- profile = SmbcloudAuthService.me(access_token: token)
+ @me_profile = SmbcloudAuthService.me(access_token: token)
@access_token = token
- @current_user = User.find_or_create_from_smbcloud(profile, access_token: token)
- rescue SmbcloudAuthService::AuthenticationError => e
- render json: { status: "error", error_code: e.error_code,
- message: "Invalid or expired access token." },
- status: :unauthorized
+ @current_user = User.find_or_create_from_smbcloud(@me_profile, access_token: token)
+ rescue SmbcloudAuthService::AuthenticationError
+ render_error(ERR_UNAUTHORIZED, "Invalid or expired access token.", status: :unauthorized)
end
def bearer_token
@@ -49,46 +49,25 @@ module Api
@current_user
end
- # Public profile shape returned to API clients.
- def user_json(user)
- {
- id: user.smbcloud_id,
- email: user.email,
- username: user.username,
- display_name: user.display_name_or_username,
- avatar_url: user.avatar_url_or_default
- }
- end
-
- def render_error(message, status:, error_code: nil)
- render json: { status: "error", error_code: error_code, message: message }, status: status
+ # ErrorResponse::Error — { error_code: <i32>, message: }
+ def render_error(error_code, message, status:)
+ render json: { error_code: error_code, message: message }, status: status
end
def render_auth_error(error)
- render json: { status: "error", error_code: error.error_code, message: error.message },
- status: :unauthorized
- end
-
- def render_not_found(error)
- render json: { status: "not_found", error_code: error.error_code, message: error.message },
- status: :not_found
- end
-
- def render_incomplete(error)
- render json: { status: "incomplete", error_code: error.error_code, message: error.message },
- status: :unprocessable_entity
+ # The gem's error_code is already an error_codes::ErrorCode (i32); pass it
+ # through so e.g. network errors surface as NetworkError, not Unauthorized.
+ code = error.error_code.is_a?(Integer) ? error.error_code : ERR_UNAUTHORIZED
+ render_error(code, error.message.presence || "Unauthorized.", status: :unauthorized)
end
def render_record_invalid(error)
- render json: { status: "error", error_code: nil, message: error.record.errors.full_messages.to_sentence },
- status: :unprocessable_entity
+ render_error(ERR_INVALID, error.record.errors.full_messages.to_sentence, status: :unprocessable_entity)
end
def render_config_error(error)
Rails.logger.error("smbCloud configuration error: #{error.message}")
- render json: { status: "error", error_code: nil,
- message: "Authentication service is not configured." },
- status: :internal_server_error
+ render_error(ERR_UNKNOWN, "Authentication service is not configured.", status: :internal_server_error)
end
end
end
app/controllers/api/v1/confirmations_controller.rb
+24
new file mode 100644
index 0000000..136340f
--- /dev/null
+++ b/app/controllers/api/v1/confirmations_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Api
+ module V1
+ # Resend the email-confirmation link for an unconfirmed account.
+ class ConfirmationsController < Api::BaseController
+ # POST /api/v1/auth/confirmation/resend
+ # Params: email
+ #
+ # The underlying endpoint always responds the same way regardless of
+ # whether the account exists or is already confirmed (no enumeration), so
+ # this returns 204 on success.
+ def create
+ email = params[:email].to_s.strip.downcase
+ if email.blank?
+ return render_error(ERR_INVALID, "Email is required.", status: :unprocessable_entity)
+ end
+
+ SmbcloudAuthService.resend_confirmation(email: email)
+ head :no_content
+ end
+ end
+ end
+end
app/controllers/api/v1/me_controller.rb
+18
-5
index 33d269f..d9fc55f 100644
--- a/app/controllers/api/v1/me_controller.rb
+++ b/app/controllers/api/v1/me_controller.rb
@@ -6,19 +6,32 @@ module Api
class MeController < Api::BaseController
before_action :authenticate_token!
- # GET /api/v1/me
+ # GET /api/v1/me — returns User { id, email, created_at, updated_at }.
# Header: Authorization: Bearer <access_token>
def show
- render json: { status: "ready", user: user_json(current_user) }, status: :ok
+ render json: user_profile_json, status: :ok
end
- # DELETE /api/v1/me
+ # DELETE /api/v1/me — permanently removes the smbCloud account and the
+ # local mirror. Returns 204 on success.
# Header: Authorization: Bearer <access_token>
- # Permanently removes the smbCloud account and the local mirror.
def destroy
SmbcloudAuthService.remove(access_token: @access_token)
current_user.destroy
- render json: { status: "ok" }, status: :ok
+ head :no_content
+ end
+
+ private
+
+ # The bare smbCloud profile, matching the desktop's `User` type.
+ def user_profile_json
+ p = @me_profile.respond_to?(:symbolize_keys) ? @me_profile.symbolize_keys : @me_profile
+ {
+ id: p[:id],
+ email: p[:email],
+ created_at: p[:created_at],
+ updated_at: p[:updated_at]
+ }
end
end
end
app/controllers/api/v1/registrations_controller.rb
+17
-12
index aacdba0..001bdea 100644
--- a/app/controllers/api/v1/registrations_controller.rb
+++ b/app/controllers/api/v1/registrations_controller.rb
@@ -4,31 +4,36 @@ module Api
module V1
# Token-based sign up for API clients.
class RegistrationsController < Api::BaseController
- # POST /api/v1/auth/sign_up
+ # POST /api/v1/auth/sign_up — returns SignupResult.
# Params: email, password
def create
email = params[:email].to_s.strip.downcase
password = params[:password].to_s
if email.blank? || password.blank?
- return render_error("Email and password are required.", status: :unprocessable_entity)
+ return render_error(ERR_INVALID, "Email and password are required.", status: :unprocessable_entity)
end
if password.length < 8
- return render_error("Password must be at least 8 characters.", status: :unprocessable_entity)
+ return render_error(ERR_INVALID, "Password must be at least 8 characters.", status: :unprocessable_entity)
end
- SmbcloudAuthService.signup(email: email, password: password)
+ result = SmbcloudAuthService.signup(email: email, password: password)
+ render json: signup_result_json(result), status: :ok
+ end
- # Try to log straight in. If the account needs email verification first,
- # login raises AccountIncompleteError → rendered as "incomplete" so the
- # client can route the user to the verify-email step.
- access_token = SmbcloudAuthService.login(email: email, password: password)
- profile = SmbcloudAuthService.me(access_token: access_token)
- user = User.find_or_create_from_smbcloud(profile, access_token: access_token)
+ private
- render json: { status: "ready", access_token: access_token, user: user_json(user) },
- status: :created
+ # SignupResult — { code: Option<i32>, message: String, data: Option<Data> }.
+ # The gem already returns this shape; normalise keys and guarantee a message.
+ def signup_result_json(result)
+ result = result.respond_to?(:symbolize_keys) ? result.symbolize_keys : result.to_h
+ {
+ code: result[:code],
+ message: result[:message].presence ||
+ "Signed up successfully. Please check your email to confirm your account.",
+ data: result[:data]
+ }
end
end
end
app/controllers/api/v1/sessions_controller.rb
+24
-9
index e8939d2..0db164a 100644
--- a/app/controllers/api/v1/sessions_controller.rb
+++ b/app/controllers/api/v1/sessions_controller.rb
@@ -6,31 +6,46 @@ module Api
class SessionsController < Api::BaseController
before_action :authenticate_token!, only: :destroy
- # POST /api/v1/auth/sign_in
+ # POST /api/v1/auth/sign_in — returns AccountStatus.
# Params: email, password
def create
email = params[:email].to_s.strip.downcase
password = params[:password].to_s
if email.blank? || password.blank?
- return render_error("Email and password are required.", status: :unprocessable_entity)
+ return render_error(ERR_INVALID, "Email and password are required.", status: :unprocessable_entity)
end
- # Raises AccountNotFoundError / AccountIncompleteError / AuthenticationError,
- # each mapped to the matching JSON shape by Api::BaseController.
access_token = SmbcloudAuthService.login(email: email, password: password)
- profile = SmbcloudAuthService.me(access_token: access_token)
- user = User.find_or_create_from_smbcloud(profile, access_token: access_token)
- render json: { status: "ready", access_token: access_token, user: user_json(user) },
- status: :ok
+ # Keep the web account page in sync by upserting the local mirror.
+ profile = SmbcloudAuthService.me(access_token: access_token)
+ User.find_or_create_from_smbcloud(profile, access_token: access_token)
+
+ # AccountStatus::Ready
+ render json: { Ready: { access_token: access_token } }, status: :ok
+ rescue SmbcloudAuthService::AccountNotFoundError
+ # AccountStatus::NotFound (serialised as the bare JSON string "NotFound")
+ render json: "NotFound".to_json, status: :ok
+ rescue SmbcloudAuthService::AccountIncompleteError => e
+ # AccountStatus::Incomplete { status: <account::ErrorCode u32> }
+ render json: { Incomplete: { status: incomplete_status_code(e) } }, status: :ok
end
# DELETE /api/v1/auth/sign_out
# Header: Authorization: Bearer <access_token>
def destroy
SmbcloudAuthService.logout(access_token: @access_token)
- render json: { status: "ok" }, status: :ok
+ head :no_content
+ end
+
+ private
+
+ # Map the gem's error_code to a valid account::ErrorCode (u32); default to
+ # EmailUnverified, the canonical "incomplete / verify your email" case.
+ def incomplete_status_code(error)
+ code = error.error_code.to_i
+ ACCOUNT_ERROR_CODES.include?(code) ? code : ACCOUNT_EMAIL_UNVERIFIED
end
end
end
app/services/smbcloud_auth_service.rb
+4
-1
index 611fd84..8c3f009 100644
--- a/app/services/smbcloud_auth_service.rb
+++ b/app/services/smbcloud_auth_service.rb
@@ -191,7 +191,10 @@ class SmbcloudAuthService
rescue AuthenticationError, KeyError
raise
rescue StandardError => e
- raise AuthenticationError.new(e.message)
+ # Log the detail server-side; return a clean message so internal infra
+ # (hosts, ports) never leaks to API clients or the flash.
+ Rails.logger.warn("smbCloud request to #{path} failed: #{e.class}: #{e.message}")
+ raise AuthenticationError.new("Network error contacting the authentication service.")
end
def self.smbcloud_app_id
config/routes.rb
+1
index e0d304a..c73c107 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,7 @@ Rails.application.routes.draw do
post "auth/sign_in", to: "sessions#create"
delete "auth/sign_out", to: "sessions#destroy"
post "auth/sign_up", to: "registrations#create"
+ post "auth/confirmation/resend", to: "confirmations#create"
get "me", to: "me#show"
delete "me", to: "me#destroy"
end