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 new
+172
@@ -0,0 +1,172 @@
1 +---
2 +name: deployment
3 +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.
4 +---
5 +
6 +# Deploying sigit.si
7 +
8 +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.
9 +
10 +## Server facts
11 +
12 +| Detail | Value |
13 +| ------ | ----- |
14 +| Domain | `sigit.si` (also the landing page `getsiti.5mb.app`) |
15 +| Host | `api.splitfire.ai` = `65.21.240.91` (Hetzner Debian, `debian-4gb-hel1-2`) |
16 +| SSH | `ssh smb1-deploy` (user `deploy`); the app runs as user `git` |
17 +| App dir | `/home/git/apps/sigitsi` (deployed in place, not Capistrano releases) |
18 +| Bare repo | `/home/git/sigitsi.git` (push target; `post-receive` hook deploys) |
19 +| Puma port | 3015 (`SIGITSI_PORT`), single mode |
20 +| Ruby | 3.4.2 via rbenv at `/home/git/.rbenv` |
21 +| User repos | `/home/git/repos/users/<username>/<repo>.git` (`SIGITSI_REPOS_PATH`) |
22 +| nginx vhost | `/etc/nginx/sites-enabled/getsiti.5mb.app` (proxies to 3015) |
23 +
24 +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.
25 +
26 +### Acting as the app user
27 +
28 +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:
29 +
30 +```bash
31 +ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && <command>"'
32 +```
33 +
34 +There is no `.env` file; env vars come from `git`'s login shell (`~/.profile`).
35 +
36 +## Deploying
37 +
38 +From a local clone with the `main` branch ready:
39 +
40 +```bash
41 +git push <prod-remote> main
42 +```
43 +
44 +The `post-receive` hook (`/home/git/sigitsi.git/hooks/post-receive`) then:
45 +
46 +1. `git checkout -f main` into `/home/git/apps/sigitsi`
47 +2. `rbenv local`, `nvm use`, `bundle install`
48 +3. `rake assets:precompile`
49 +4. `rake db:prepare`
50 +5. `kill -9 $(lsof -t -i:3015)` then `bundle exec puma -e production > output.log 2>&1 &`
51 +6. `bin/jobs start > output-jobs.log 2>&1 &`
52 +
53 +### The hook does not reliably migrate (read this before trusting a deploy)
54 +
55 +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.
56 +
57 +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".
58 +
59 +## Run a migration
60 +
61 +The deploy hook's `rake db:prepare` now self-migrates (all four databases exist; see "Postgres admin"). To migrate by hand:
62 +
63 +```bash
64 +# check what is pending first
65 +ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate:status | tail"'
66 +
67 +# apply
68 +ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:migrate"'
69 +```
70 +
71 +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`.
72 +
73 +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.
74 +
75 +## Seed production data
76 +
77 +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.
78 +
79 +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:
80 +
81 +```bash
82 +# normal
83 +ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails db:seed"'
84 +# fallback if the solid DBs are missing
85 +ssh smb1-deploy 'sudo -u git bash -lc "cd /home/git/apps/sigitsi && bin/rails runner \"Rails.application.load_seed\""'
86 +```
87 +
88 +What it creates (`db/seeds.rb`, idempotent — safe to re-run):
89 +
90 +- Demo owner users (`bartowski`, `meta-llama`, `sentence-transformers`) and one model under the existing `sigit` user (`SiGit-Coder-1.5B-GGUF`).
91 +- 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.
92 +
93 +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`.
94 +
95 +Verify:
96 +
97 +```bash
98 +curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Chrome/130)" https://sigit.si/bartowski/Qwen2.5-3B-Instruct-GGUF
99 +```
100 +
101 +## Restart Puma
102 +
103 +Single-mode Puma on 3015. Two options:
104 +
105 +```bash
106 +# Preferred: hot restart (re-exec, ~zero downtime). Get the master pid:
107 +ssh smb1-deploy 'ss -ltnp | grep 3015' # or: sudo -u git lsof -t -i:3015
108 +ssh smb1-deploy 'sudo -u git kill -USR2 <master_pid>'
109 +
110 +# Full restart (matches the deploy hook), if a hot restart misbehaves:
111 +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 &"'
112 +```
113 +
114 +Confirm it rebound and serves:
115 +
116 +```bash
117 +ssh smb1-deploy 'ss -ltnp | grep 3015'
118 +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'
119 +```
120 +
121 +There is no systemd unit; Puma is a detached background process owned by `git`.
122 +
123 +## nginx
124 +
125 +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`).
126 +
127 +### The static-extension location must fall through to the app
128 +
129 +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:
130 +
131 +```nginx
132 +location ~ ^(?!/rails/).+\.(jpg|jpeg|gif|png|ico|json|txt|xml)$ {
133 + ...
134 + try_files $uri @puma15; # NOT =404
135 +}
136 +```
137 +
138 +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.
139 +
140 +### Editing the vhost safely
141 +
142 +- `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`.
143 +- Always `sudo nginx -t` before `sudo systemctl reload nginx`.
144 +
145 +## Debugging a 500 after deploy
146 +
147 +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).
148 +2. `bin/rails db:migrate:status` to see if a migration is `down`.
149 +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.
150 +
151 +## Postgres admin
152 +
153 +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`:
154 +
155 +```bash
156 +ssh smb1-deploy 'sudo -u postgres psql -c "<SQL>"'
157 +```
158 +
159 +The four databases (`sigitsi_production` plus the solid `_cache` / `_queue` / `_cable`) now exist, each owned by `sigitsi`, created with:
160 +
161 +```sql
162 +CREATE DATABASE sigitsi_production_cache OWNER sigitsi;
163 +CREATE DATABASE sigitsi_production_queue OWNER sigitsi;
164 +CREATE DATABASE sigitsi_production_cable OWNER sigitsi;
165 +```
166 +
167 +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).
168 +
169 +## Known issues
170 +
171 +- **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.
172 +- **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
@@ -3,42 +3,42 @@
3 module Api
4 # Base for all JSON API controllers.
5 #
6 - # Unlike the HTML controllers, the API is token-based: clients (e.g. the
7 - # siGit Code & Deploy desktop app) send the smbCloud access token as
8 - # `Authorization: Bearer <token>` instead of relying on the Rails session
9 - # cookie. Response shapes mirror the desktop client's AccountStatus contract:
6 + # The API is token-based: clients (the siGit Code & Deploy desktop app) send
7 + # the smbCloud access token as `Authorization: Bearer <token>`. Response bodies
8 + # match the desktop's shared-model Rust types (smbcloud-model) exactly, so the
9 + # app deserializes them directly:
10 #
11 - # ready → { status: "ready", access_token:, user: {...} }
12 - # not_found → { status: "not_found", error_code:, message: }
13 - # incomplete → { status: "incomplete", error_code:, message: }
14 - # error → { status: "error", error_code:, message: }
11 + # AccountStatus → "NotFound" | {"Ready":{"access_token":…}} | {"Incomplete":{"status":<u32>}}
12 + # User → {"id":,"email":,"created_at":,"updated_at":}
13 + # SignupResult → {"code":,"message":,"data":{…}}
14 + # ErrorResponse → {"error_code":<i32>,"message":}
15 class BaseController < ActionController::API
16 - # Order matters: rescue_from is matched in reverse declaration order, so the
17 - # base AuthenticationError is declared first and its subclasses after, which
18 - # makes the more specific handlers win.
19 - rescue_from SmbcloudAuthService::AuthenticationError, with: :render_auth_error
20 - rescue_from SmbcloudAuthService::AccountNotFoundError, with: :render_not_found
21 - rescue_from SmbcloudAuthService::AccountIncompleteError, with: :render_incomplete
22 - rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
23 - rescue_from KeyError, with: :render_config_error
16 + # error_codes::ErrorCode (i32) — used in ErrorResponse.
17 + ERR_UNKNOWN = 0
18 + ERR_UNAUTHORIZED = 100
19 + ERR_INVALID = 101
20 + # account::ErrorCode (u32) — used in AccountStatus::Incomplete.status.
21 + ACCOUNT_ERROR_CODES = [ 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007 ].freeze
22 + ACCOUNT_EMAIL_UNVERIFIED = 1001
23 +
24 + rescue_from SmbcloudAuthService::AuthenticationError, with: :render_auth_error
25 + rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
26 + rescue_from KeyError, with: :render_config_error
27
28 private
29
27 - # Verifies the bearer token against smbCloud and upserts the local user.
28 - # On success sets @access_token and @current_user; otherwise halts with 401.
30 + # Verifies the bearer token against smbCloud, exposes the profile in
31 + # @me_profile, and upserts the local mirror. On failure halts with an
32 + # ErrorResponse (401).
33 def authenticate_token!
34 token = bearer_token
31 - if token.blank?
32 - return render_error("Missing access token.", status: :unauthorized)
33 - end
35 + return render_error(ERR_UNAUTHORIZED, "Missing access token.", status: :unauthorized) if token.blank?
36
35 - profile = SmbcloudAuthService.me(access_token: token)
37 + @me_profile = SmbcloudAuthService.me(access_token: token)
38 @access_token = token
37 - @current_user = User.find_or_create_from_smbcloud(profile, access_token: token)
38 - rescue SmbcloudAuthService::AuthenticationError => e
39 - render json: { status: "error", error_code: e.error_code,
40 - message: "Invalid or expired access token." },
41 - status: :unauthorized
39 + @current_user = User.find_or_create_from_smbcloud(@me_profile, access_token: token)
40 + rescue SmbcloudAuthService::AuthenticationError
41 + render_error(ERR_UNAUTHORIZED, "Invalid or expired access token.", status: :unauthorized)
42 end
43
44 def bearer_token
@@ -49,46 +49,25 @@ module Api
49 @current_user
50 end
51
52 - # Public profile shape returned to API clients.
53 - def user_json(user)
54 - {
55 - id: user.smbcloud_id,
56 - email: user.email,
57 - username: user.username,
58 - display_name: user.display_name_or_username,
59 - avatar_url: user.avatar_url_or_default
60 - }
61 - end
62 -
63 - def render_error(message, status:, error_code: nil)
64 - render json: { status: "error", error_code: error_code, message: message }, status: status
52 + # ErrorResponse::Error — { error_code: <i32>, message: }
53 + def render_error(error_code, message, status:)
54 + render json: { error_code: error_code, message: message }, status: status
55 end
56
57 def render_auth_error(error)
68 - render json: { status: "error", error_code: error.error_code, message: error.message },
69 - status: :unauthorized
70 - end
71 -
72 - def render_not_found(error)
73 - render json: { status: "not_found", error_code: error.error_code, message: error.message },
74 - status: :not_found
75 - end
76 -
77 - def render_incomplete(error)
78 - render json: { status: "incomplete", error_code: error.error_code, message: error.message },
79 - status: :unprocessable_entity
58 + # The gem's error_code is already an error_codes::ErrorCode (i32); pass it
59 + # through so e.g. network errors surface as NetworkError, not Unauthorized.
60 + code = error.error_code.is_a?(Integer) ? error.error_code : ERR_UNAUTHORIZED
61 + render_error(code, error.message.presence || "Unauthorized.", status: :unauthorized)
62 end
63
64 def render_record_invalid(error)
83 - render json: { status: "error", error_code: nil, message: error.record.errors.full_messages.to_sentence },
84 - status: :unprocessable_entity
65 + render_error(ERR_INVALID, error.record.errors.full_messages.to_sentence, status: :unprocessable_entity)
66 end
67
68 def render_config_error(error)
69 Rails.logger.error("smbCloud configuration error: #{error.message}")
89 - render json: { status: "error", error_code: nil,
90 - message: "Authentication service is not configured." },
91 - status: :internal_server_error
70 + render_error(ERR_UNKNOWN, "Authentication service is not configured.", status: :internal_server_error)
71 end
72 end
73 end
app/controllers/api/v1/confirmations_controller.rb new
+24
@@ -0,0 +1,24 @@
1 +# frozen_string_literal: true
2 +
3 +module Api
4 + module V1
5 + # Resend the email-confirmation link for an unconfirmed account.
6 + class ConfirmationsController < Api::BaseController
7 + # POST /api/v1/auth/confirmation/resend
8 + # Params: email
9 + #
10 + # The underlying endpoint always responds the same way regardless of
11 + # whether the account exists or is already confirmed (no enumeration), so
12 + # this returns 204 on success.
13 + def create
14 + email = params[:email].to_s.strip.downcase
15 + if email.blank?
16 + return render_error(ERR_INVALID, "Email is required.", status: :unprocessable_entity)
17 + end
18 +
19 + SmbcloudAuthService.resend_confirmation(email: email)
20 + head :no_content
21 + end
22 + end
23 + end
24 +end
app/controllers/api/v1/me_controller.rb
+18 -5
@@ -6,19 +6,32 @@ module Api
6 class MeController < Api::BaseController
7 before_action :authenticate_token!
8
9 - # GET /api/v1/me
9 + # GET /api/v1/me — returns User { id, email, created_at, updated_at }.
10 # Header: Authorization: Bearer <access_token>
11 def show
12 - render json: { status: "ready", user: user_json(current_user) }, status: :ok
12 + render json: user_profile_json, status: :ok
13 end
14
15 - # DELETE /api/v1/me
15 + # DELETE /api/v1/me — permanently removes the smbCloud account and the
16 + # local mirror. Returns 204 on success.
17 # Header: Authorization: Bearer <access_token>
17 - # Permanently removes the smbCloud account and the local mirror.
18 def destroy
19 SmbcloudAuthService.remove(access_token: @access_token)
20 current_user.destroy
21 - render json: { status: "ok" }, status: :ok
21 + head :no_content
22 + end
23 +
24 + private
25 +
26 + # The bare smbCloud profile, matching the desktop's `User` type.
27 + def user_profile_json
28 + p = @me_profile.respond_to?(:symbolize_keys) ? @me_profile.symbolize_keys : @me_profile
29 + {
30 + id: p[:id],
31 + email: p[:email],
32 + created_at: p[:created_at],
33 + updated_at: p[:updated_at]
34 + }
35 end
36 end
37 end
app/controllers/api/v1/registrations_controller.rb
+17 -12
@@ -4,31 +4,36 @@ module Api
4 module V1
5 # Token-based sign up for API clients.
6 class RegistrationsController < Api::BaseController
7 - # POST /api/v1/auth/sign_up
7 + # POST /api/v1/auth/sign_up — returns SignupResult.
8 # Params: email, password
9 def create
10 email = params[:email].to_s.strip.downcase
11 password = params[:password].to_s
12
13 if email.blank? || password.blank?
14 - return render_error("Email and password are required.", status: :unprocessable_entity)
14 + return render_error(ERR_INVALID, "Email and password are required.", status: :unprocessable_entity)
15 end
16
17 if password.length < 8
18 - return render_error("Password must be at least 8 characters.", status: :unprocessable_entity)
18 + return render_error(ERR_INVALID, "Password must be at least 8 characters.", status: :unprocessable_entity)
19 end
20
21 - SmbcloudAuthService.signup(email: email, password: password)
21 + result = SmbcloudAuthService.signup(email: email, password: password)
22 + render json: signup_result_json(result), status: :ok
23 + end
24
23 - # Try to log straight in. If the account needs email verification first,
24 - # login raises AccountIncompleteError → rendered as "incomplete" so the
25 - # client can route the user to the verify-email step.
26 - access_token = SmbcloudAuthService.login(email: email, password: password)
27 - profile = SmbcloudAuthService.me(access_token: access_token)
28 - user = User.find_or_create_from_smbcloud(profile, access_token: access_token)
25 + private
26
30 - render json: { status: "ready", access_token: access_token, user: user_json(user) },
31 - status: :created
27 + # SignupResult — { code: Option<i32>, message: String, data: Option<Data> }.
28 + # The gem already returns this shape; normalise keys and guarantee a message.
29 + def signup_result_json(result)
30 + result = result.respond_to?(:symbolize_keys) ? result.symbolize_keys : result.to_h
31 + {
32 + code: result[:code],
33 + message: result[:message].presence ||
34 + "Signed up successfully. Please check your email to confirm your account.",
35 + data: result[:data]
36 + }
37 end
38 end
39 end
app/controllers/api/v1/sessions_controller.rb
+24 -9
@@ -6,31 +6,46 @@ module Api
6 class SessionsController < Api::BaseController
7 before_action :authenticate_token!, only: :destroy
8
9 - # POST /api/v1/auth/sign_in
9 + # POST /api/v1/auth/sign_in — returns AccountStatus.
10 # Params: email, password
11 def create
12 email = params[:email].to_s.strip.downcase
13 password = params[:password].to_s
14
15 if email.blank? || password.blank?
16 - return render_error("Email and password are required.", status: :unprocessable_entity)
16 + return render_error(ERR_INVALID, "Email and password are required.", status: :unprocessable_entity)
17 end
18
19 - # Raises AccountNotFoundError / AccountIncompleteError / AuthenticationError,
20 - # each mapped to the matching JSON shape by Api::BaseController.
19 access_token = SmbcloudAuthService.login(email: email, password: password)
22 - profile = SmbcloudAuthService.me(access_token: access_token)
23 - user = User.find_or_create_from_smbcloud(profile, access_token: access_token)
20
25 - render json: { status: "ready", access_token: access_token, user: user_json(user) },
26 - status: :ok
21 + # Keep the web account page in sync by upserting the local mirror.
22 + profile = SmbcloudAuthService.me(access_token: access_token)
23 + User.find_or_create_from_smbcloud(profile, access_token: access_token)
24 +
25 + # AccountStatus::Ready
26 + render json: { Ready: { access_token: access_token } }, status: :ok
27 + rescue SmbcloudAuthService::AccountNotFoundError
28 + # AccountStatus::NotFound (serialised as the bare JSON string "NotFound")
29 + render json: "NotFound".to_json, status: :ok
30 + rescue SmbcloudAuthService::AccountIncompleteError => e
31 + # AccountStatus::Incomplete { status: <account::ErrorCode u32> }
32 + render json: { Incomplete: { status: incomplete_status_code(e) } }, status: :ok
33 end
34
35 # DELETE /api/v1/auth/sign_out
36 # Header: Authorization: Bearer <access_token>
37 def destroy
38 SmbcloudAuthService.logout(access_token: @access_token)
33 - render json: { status: "ok" }, status: :ok
39 + head :no_content
40 + end
41 +
42 + private
43 +
44 + # Map the gem's error_code to a valid account::ErrorCode (u32); default to
45 + # EmailUnverified, the canonical "incomplete / verify your email" case.
46 + def incomplete_status_code(error)
47 + code = error.error_code.to_i
48 + ACCOUNT_ERROR_CODES.include?(code) ? code : ACCOUNT_EMAIL_UNVERIFIED
49 end
50 end
51 end
app/services/smbcloud_auth_service.rb
+4 -1
@@ -191,7 +191,10 @@ class SmbcloudAuthService
191 rescue AuthenticationError, KeyError
192 raise
193 rescue StandardError => e
194 - raise AuthenticationError.new(e.message)
194 + # Log the detail server-side; return a clean message so internal infra
195 + # (hosts, ports) never leaks to API clients or the flash.
196 + Rails.logger.warn("smbCloud request to #{path} failed: #{e.class}: #{e.message}")
197 + raise AuthenticationError.new("Network error contacting the authentication service.")
198 end
199
200 def self.smbcloud_app_id
config/routes.rb
+1
@@ -43,6 +43,7 @@ Rails.application.routes.draw do
43 post "auth/sign_in", to: "sessions#create"
44 delete "auth/sign_out", to: "sessions#destroy"
45 post "auth/sign_up", to: "registrations#create"
46 + post "auth/confirmation/resend", to: "confirmations#create"
47 get "me", to: "me#show"
48 delete "me", to: "me#destroy"
49 end