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