Skip to content

Commit 3697dbb

Browse files
authored
feat(guard): show logged-in state on signup page with continue button (#661)
## 📝 Description Modifies signup page to display "Continue to" button for logged-in users instead of signup options, and includes comprehensive test coverage for all signup scenarios. Also adds AGENTS.md with repository guidelines for LLM agents. Check [the issue](renderedtext/tasks#8782). ## ✅ Checklist - [x] I have tested this change - [x] ~This change requires documentation update~ N/A
1 parent 68d7fee commit 3697dbb

File tree

9 files changed

+208
-46
lines changed

9 files changed

+208
-46
lines changed

guard/.sobelow-conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
verbose: false,
33
private: false,
4-
skip: false,
4+
skip: true,
55
router: "",
66
exit: "false",
77
format: "txt",

guard/AGENTS.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `lib/` hosts Guard application modules, supervisors, and generated gRPC clients in `lib/internal_api`; re-run `make pb.gen` after proto updates.
5+
- `config/` covers environment configs (`config/{dev,test,prod}.exs`) and runtime secrets; keep credentials in env vars, not source files.
6+
- `test/` mirrors `lib/` with ExUnit suites and helpers in `test/support`; service stubs and golden data live under `fixture/`.
7+
- `priv/`, `templates/`, and `assets/` hold persisted resources, mailer templates, and UI bundles, while `helm/` ships the deployment chart.
8+
9+
## Getting Started & Triage
10+
- Review `DOCUMENTATION.md` for a high-level system map, component triage tips, and debugging entry points before you dive into specific change requests.
11+
- Keep that guide handy when diagnosing incidents or onboarding new contributors; it consolidates architecture, workflows, and configuration dependencies.
12+
13+
## Build, Test, and Development Commands
14+
- `mix deps.get && mix compile` installs dependencies and ensures the code builds.
15+
- `make console.ex` opens `iex -S mix` inside the project container; use `console.bash` when you need a plain shell.
16+
- `make test.ex.setup` provisions Postgres, Redis, and RabbitMQ; run it once before the first test pass.
17+
- `make test.ex [FILE=...]` or `mix test` drives ExUnit; `mix test.watch` keeps a loop running during TDD.
18+
- `mix credo --strict`, `mix format`, and `mix sobelow --config .sobelow-conf` must pass before pushing.
19+
20+
## Coding Style & Naming Conventions
21+
- Always run the Elixir formatter (2-space indentation, max 120 columns) and rely on `.formatter.exs` for the file list.
22+
- Modules stay in `CamelCase`, functions and variables in `snake_case`; generated protobuf code remains untouched.
23+
- Share helpers through `lib/guard/support` or `test/support`, and favour pattern matching plus small pure functions for clarity.
24+
25+
## Testing Guidelines
26+
- Place ExUnit specs next to the code (`*_test.exs`) and organise scenarios with `describe` blocks.
27+
- Reuse factories, Mox doubles, and helpers from `test/support`; keep `fixture/` data deterministic and updated with contract changes.
28+
- Tag slow external calls with `@tag :integration`, supply skip guards, and document any required services in the PR.
29+
30+
## Commit & Pull Request Guidelines
31+
- Follow the existing convention `type(guard): imperative summary (#ticket)` (example: `feat(guard): add team SSO (#660)`).
32+
- Keep commits focused; include migrations or protobuf updates alongside the change and flag remaining work explicitly.
33+
- Pull requests should explain motivation, list validation commands (`make test.ex`, `mix credo`), link Semaphore issues, and attach UI screenshots or config notes when relevant.
34+
35+
## Security & Configuration Tips
36+
- Use the environment block in `Makefile` when running locally and override secrets through exported variables instead of committing changes.
37+
- Before merging, run `mix sobelow --config .sobelow-conf --exit` and review gRPC regen diffs for unintended data exposure.

guard/DOCUMENTATION.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Guard Agent Playbook
2+
3+
## Overview
4+
- `Guard` is an Elixir OTP application that fronts user auth, organization lifecycle, and git-provider integrations for Semaphore.
5+
- Primary entry point is `lib/guard/application.ex`; it wires repos, optional feature provider workers, GRPC servers, HTTP Plug endpoints, and Cachex caches based on `START_*` env flags.
6+
- Services expose GRPC APIs (port `50051`/`50052`) and HTTP endpoints (ports `4003`/`4004`) toggled by system env, so inspect those vars before enabling features locally.
7+
8+
## Persistence & Data Shape
9+
- Three Ecto repos: `Guard.Repo`, `Guard.FrontRepo`, and `Guard.InstanceConfigRepo` (all configured via `config/config.exs`). Migrations live in `priv/repo/migrations` and `priv/front_repo/migrations`.
10+
- The main repo embeds schema modules inside `lib/guard/repo.ex` (Users, Collaborators, Projects, ProjectMembers, Suspensions) and additional data-layer logic resides under `lib/guard/store/`.
11+
- Cachex caches (`:ppl_cache`, `:feature_provider_cache`, `:config_cache`) are started conditionally; clear them in tests by dropping the Cachex tables if behaviour appears sticky.
12+
13+
## Domain Map
14+
- `lib/guard/api/` contains external provider clients built with Tesla (`Guard.Api.Github`, `Guard.Api.Bitbucket`, `Guard.Api.Okta`, etc.).
15+
- `lib/guard/grpc_servers/` hosts GRPC server implementations; each wraps protobuf modules from `lib/internal_api/**`.
16+
- `lib/guard/id/`, `lib/guard/oidc/`, `lib/guard/authentication_token.ex`, and `lib/guard/session.ex` compose the HTTP identity endpoints.
17+
- `lib/guard/services/` contains background workers (RabbitMQ consumers, invalidators) supervised by the application.
18+
- `templates/` and `assets/` hold the minimal web UI for login/blocked flows; `priv/` contains persistent resources and embedded repos.
19+
20+
## Local Workflows
21+
- Bootstrap: `mix deps.get && mix compile`.
22+
- Console work: `make console.ex` (starts `iex -S mix` inside the Docker toolchain), or `console.bash` when you need a raw shell.
23+
- Database bootstrap: `make test.ex.setup` (creates/migrates Postgres via docker-compose and seeds broker dependencies).
24+
- Tests: `make test.ex [FILE=path/to/test.exs]` or `mix test`; `mix test.watch` is available for TDD loops. CI uses `JUnitFormatter` (see `test/test_helper.exs`).
25+
- Quality gates: `mix format`, `mix credo --strict`, and `mix sobelow --config .sobelow-conf --exit`. Credo config lives in `.credo.exs`; formatter inputs are defined in `.formatter.exs`.
26+
- Regenerate protobufs after updating `renderedtext/internal_api` definitions with `make pb.gen` (requires Docker access and SSH credentials).
27+
28+
## Testing Toolkit
29+
- Shared fixtures, factories, and Mox mocks live under `test/support/**`; use those instead of rolling bespoke stubs.
30+
- HTTP/service doubles sit in `test/fake` and `test/fixture`, while async scenarios reuse helpers in `test/support/wait.ex` and `test/support/concurrent_repo_case.ex`.
31+
- ExVCR is available for capturing HTTP interactions; prefer deterministic fixtures and keep cassettes under version control if used.
32+
33+
## Configuration Notes
34+
- Base config is in `config/config.exs`; environment-specific overrides live in `config/{dev,test,prod}.exs` and `config/runtime.exs`.
35+
- Many behaviours hinge on env vars exported via the `Makefile` (e.g., `START_GRPC_*`, `START_FEATURE_PROVIDER`, `BASE_DOMAIN`, `AMQP_URL`). Override them in the shell instead of editing config files.
36+
- `docker-compose.yml` describes the dev stack (Elixir app + Postgres 9.6 + RabbitMQ + Adminer); ensure the local Docker daemon runs before invoking `make console.ex`.
37+
- Metrics use `watchman` (StatsD) with namespace `guard.<env>`, and Sentry logging is enabled in production.
38+
39+
## Debugging Patterns
40+
- GRPC servers run under `GRPC.Server.Supervisor`; check logs on ports `50051`/`50052` if clients cannot connect.
41+
- RabbitMQ consumers (`Guard.Services.Organization*`) depend on `AMQP_URL`; missed events usually mean the queue wasn't configured in config or broker is down.
42+
- For OAuth/Git providers, secrets come from `Guard.GitProviderCredentials`; ensure appropriate vault/config entries exist when tokens cannot refresh.
43+
- `Guard.Migrator` offers helper functions for cross-repo migrations—search it when maintaining legacy data.

guard/config/config.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ config :watchman,
2525
config :guard,
2626
projecthub_grpc_endpoint: "127.0.0.1:50052",
2727
organization_grpc_endpoint: "127.0.0.1:50052",
28+
# sobelow_skip ["Config.Secrets"]
2829
secrethub_grpc_endpoint: "127.0.0.1:50052",
2930
pipeline_grpc_endpoint: "127.0.0.1:50052",
3031
repo_proxy_grpc_endpoint: "127.0.0.1:50052",

guard/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ services:
6666

6767
volumes:
6868
- .:/app
69+
- /app/deps
70+
- /app/_build
6971

7072
db:
7173
image: postgres:9.6

guard/lib/guard/id/api.ex

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -139,29 +139,30 @@ defmodule Guard.Id.Api do
139139
# Signup endpoint
140140
#
141141
get "/signup" do
142-
ensure_empty_user(conn, fn ->
143-
case default_login_method() do
144-
method when method in ["local", "oidc"] ->
145-
conn |> signup_page(method)
142+
logged_in = conn.assigns[:user_id] != nil
146143

147-
unknown ->
148-
Logger.error("Unknown default signup method: #{unknown}")
144+
case default_login_method() do
145+
method when method in ["local", "oidc"] ->
146+
conn |> signup_page(method, logged_in)
149147

150-
conn |> error_login_page("Signup is disabled")
151-
end
152-
end)
148+
unknown ->
149+
Logger.error("Unknown default signup method: #{unknown}")
150+
conn |> error_login_page("Signup is disabled")
151+
end
153152
end
154153

155-
defp signup_page(conn, "local") do
154+
defp signup_page(conn, "local", logged_in) do
156155
conn
157156
|> render_signup_page(
158157
github: id_page("github"),
159158
bitbucket: id_page("bitbucket"),
160-
gitlab: id_page("gitlab") |> filter_gitlab()
159+
gitlab: id_page("gitlab") |> filter_gitlab(),
160+
logged_in: logged_in,
161+
me_url: me_page()
161162
)
162163
end
163164

164-
defp signup_page(conn, "oidc") do
165+
defp signup_page(conn, "oidc", logged_in) do
165166
if Guard.OIDC.enabled?() do
166167
oidc_callback = id_page("oidc/callback")
167168

@@ -172,7 +173,9 @@ defmodule Guard.Id.Api do
172173
|> render_signup_page(
173174
github: "#{url}&kc_idp_hint=github",
174175
bitbucket: "#{url}&kc_idp_hint=bitbucket",
175-
gitlab: "#{url}&kc_idp_hint=gitlab" |> filter_gitlab()
176+
gitlab: "#{url}&kc_idp_hint=gitlab" |> filter_gitlab(),
177+
logged_in: logged_in,
178+
me_url: me_page()
176179
)
177180

178181
{:error, error} ->
@@ -187,6 +190,7 @@ defmodule Guard.Id.Api do
187190
end
188191
end
189192

193+
# sobelow_skip ["XSS.SendResp"]
190194
defp render_signup_page(conn, assigns) do
191195
assigns =
192196
Keyword.merge(assigns,
@@ -331,6 +335,7 @@ defmodule Guard.Id.Api do
331335
end
332336
end
333337

338+
# sobelow_skip ["XSS.SendResp"]
334339
defp render_login_page(conn, assigns) do
335340
assigns =
336341
Keyword.merge(assigns,

guard/lib/guard/instance_config/api.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ defmodule Guard.InstanceConfig.Api do
185185
end
186186
end
187187

188+
# sobelow_skip ["XSS.SendResp"]
188189
defp render_manifest_page(conn, assigns) do
189190
html_content =
190191
Guard.TemplateRenderer.render_template([assigns: assigns], "submit_manifest.html")

guard/templates/signup.html.eex

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -80,40 +80,53 @@
8080
</div>
8181
<div class="loggin">
8282
<div class="wrap">
83-
<h2 class="has-text-align-center" style="margin-top: 0;">Try Cloud</h2>
84-
<div class="wp-block-buttons split-button is-layout-flex wp-block-buttons-is-layout-flex">
85-
<%= if assigns[:github] do %>
86-
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
87-
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color wp-element-button"
88-
href="<%= @github %>"
89-
style="border-color:#CCCCCC;border-width:1px;border-radius:8px">
90-
<img decoding="async" width="22" height="22" class="wp-image-23604" style="width: 22px;" src="https://semaphoreci.com/wp-content/uploads/2024/04/github.svg" alt="">
91-
<strong>Signup with GitHub</strong>
92-
</a>
83+
<%= if assigns[:logged_in] do %>
84+
<h2 class="has-text-align-center" style="margin-top: 0;">You're already logged in</h2>
85+
<div class="wp-block-buttons split-button is-layout-flex wp-block-buttons-is-layout-flex">
86+
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
87+
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color has-border-color wp-element-button"
88+
href="<%= @me_url %>"
89+
style="border-color:#CCCCCC;border-width:1px;border-radius:8px;display:flex;align-items:center;justify-content:center;">
90+
<strong style="display:flex;align-items:center;">Continue to <img decoding="async" src="https://semaphore.io/wp-content/uploads/2024/04/semaphore-logo.svg" alt="Semaphore" style="height: 18px; margin-left: 6px;"></strong>
91+
</a>
92+
</div>
9393
</div>
94-
<% end %>
95-
<%= if assigns[:bitbucket] do %>
96-
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
97-
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color has-border-color wp-element-button"
98-
style="border-color:#CCCCCC;border-width:1px;border-radius:8px"
99-
href="<%= @bitbucket %>">
100-
<img decoding="async" width="20" height="19" class="wp-image-23603" style="width: 20px;" src="https://semaphoreci.com/wp-content/uploads/2024/04/bitbucket.svg" alt="">
101-
<strong>Signup with Bitbucket</strong>
102-
</a>
94+
<% else %>
95+
<h2 class="has-text-align-center" style="margin-top: 0;">Try Cloud</h2>
96+
<div class="wp-block-buttons split-button is-layout-flex wp-block-buttons-is-layout-flex">
97+
<%= if assigns[:github] do %>
98+
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
99+
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color wp-element-button"
100+
href="<%= @github %>"
101+
style="border-color:#CCCCCC;border-width:1px;border-radius:8px">
102+
<img decoding="async" width="22" height="22" class="wp-image-23604" style="width: 22px;" src="https://semaphoreci.com/wp-content/uploads/2024/04/github.svg" alt="">
103+
<strong>Signup with GitHub</strong>
104+
</a>
105+
</div>
106+
<% end %>
107+
<%= if assigns[:bitbucket] do %>
108+
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
109+
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color has-border-color wp-element-button"
110+
style="border-color:#CCCCCC;border-width:1px;border-radius:8px"
111+
href="<%= @bitbucket %>">
112+
<img decoding="async" width="20" height="19" class="wp-image-23603" style="width: 20px;" src="https://semaphoreci.com/wp-content/uploads/2024/04/bitbucket.svg" alt="">
113+
<strong>Signup with Bitbucket</strong>
114+
</a>
115+
</div>
116+
<% end %>
117+
<%= if assigns[:gitlab] do %>
118+
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
119+
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color has-border-color wp-element-button"
120+
style="border-color:#CCCCCC;border-width:1px;border-radius:8px"
121+
href="<%= @gitlab %>">
122+
<img decoding="async" width="20" height="19" class="wp-image-23603" style="width: 20px;" src="https://semaphoreci.com/wp-content/uploads/2025/02/gitlab.svg" alt="">
123+
<strong>Signup with GitLab</strong>
124+
</a>
125+
</div>
126+
<% end %>
103127
</div>
104-
<% end %>
105-
<%= if assigns[:gitlab] do %>
106-
<div class="wp-block-button has-custom-width wp-block-button__width-100 has-custom-font-size is-style-v8-dropshadow has-font-down-2-font-size">
107-
<a class="wp-block-button__link has-v-8-black-color has-v-8-white-background-color has-text-color has-background has-link-color has-border-color wp-element-button"
108-
style="border-color:#CCCCCC;border-width:1px;border-radius:8px"
109-
href="<%= @gitlab %>">
110-
<img decoding="async" width="20" height="19" class="wp-image-23603" style="width: 20px;" src="https://semaphoreci.com/wp-content/uploads/2025/02/gitlab.svg" alt="">
111-
<strong>Signup with GitLab</strong>
112-
</a>
113-
</div>
114-
<% end %>
115-
</div>
116-
<p class="has-text-align-center has-font-down-3-font-size">By continuing, you agree to our <a href="https://semaphoreci.com/tos">Terms of Service</a> and <a href="https://semaphoreci.com/privacy">Privacy policy</a>. </p>
128+
<p class="has-text-align-center has-font-down-3-font-size">By continuing, you agree to our <a href="https://semaphoreci.com/tos">Terms of Service</a> and <a href="https://semaphoreci.com/privacy">Privacy policy</a>. </p>
129+
<% end %>
117130
</div>
118131
</div>
119132
</div>

0 commit comments

Comments
 (0)