✨ (web): Show only login in header for visitors; add token copy button
Changes
8 files changed, +81 -8
MODIFY
README.md
+2 -2
@@ -14,7 +14,7 @@
14
14
- push and private read authenticate with **personal access tokens** (HTTP Basic password)
15
15
- Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
16
16
- public-key authentication only; keys managed per user in the UI
17
-- Web UI: landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files), a rendered README (commonmark-java, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
17
+- Web UI: an auth-aware header nav (a "Log in" button for visitors; Profile, SSH keys, Access tokens, and Logout links only for signed-in users), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files), a rendered README (commonmark-java, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
18
18
- Per-repository issues: title, optional description, per-repo sequential number (`#1`, `#2`, …), and author; created and managed by the repo owner, readable by anyone who can read the repo, via a dedicated "New issue" page
19
19
- Issues move through a fixed lifecycle (Planned → In development → Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
20
20
- Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
@@ -60,7 +60,7 @@
60
60
- Repository names resolve to UUID-based storage paths **through the database only**
61
61
(`<storage-root>/<owner-uuid>/<repo-uuid>.git`); HTTP, SSH, and UI share the same
62
62
resolution and authorization services.
63
-- Tokens are stored as SHA-256 hashes; the plaintext is shown exactly once at creation.
63
+- Tokens are stored as SHA-256 hashes; the plaintext is shown exactly once at creation, with a copy-to-clipboard button next to it.
64
64
- The SSH host key is generated on first start and persisted, so the host identity is
65
65
stable across restarts.
66
66
- SSH serves only `git-upload-pack`/`git-receive-pack`; shells and other commands are rejected.
MODIFY
src/main/java/de/workaround/account/CurrentUser.java
+10 -1
@@ -4,12 +4,15 @@
4
4
import io.quarkus.security.identity.SecurityIdentity;
5
5
import jakarta.enterprise.context.RequestScoped;
6
6
import jakarta.inject.Inject;
7
+import jakarta.inject.Named;
7
8
import org.eclipse.microprofile.jwt.JsonWebToken;
8
9
9
10
/**
10
11
* Resolves the logged-in UI user, provisioning the local record from token claims on access.
11
- * Returns null for anonymous requests.
12
+ * Returns null for anonymous requests. Named so templates can switch the header nav via
13
+ * {cdi:currentUser.loggedIn} without each page passing the user along.
12
14
*/
15
+@Named("currentUser")
13
16
@RequestScoped
14
17
public class CurrentUser
15
18
{
@@ -21,6 +24,12 @@
21
24
22
25
private User cached;
23
26
27
+ /** Cheap auth check for templates — no user provisioning, just the identity state. */
28
+ public boolean isLoggedIn()
29
+ {
30
+ return !identity.isAnonymous();
31
+ }
32
+
24
33
public User get()
25
34
{
26
35
if (identity.isAnonymous())
MODIFY
src/main/resources/META-INF/resources/shark.css
+13 -4
@@ -1148,6 +1148,19 @@
1148
1148
display: block;
1149
1149
}
1150
1150
1151
+/* copied confirmation for any [data-copy] button (clone dialog, token page, ...) */
1152
+.copy-btn.copied {
1153
+ color: var(--success-deep);
1154
+}
1155
+
1156
+/* one-time token reveal: plaintext with a copy button on the same line */
1157
+pre.token-line {
1158
+ display: flex;
1159
+ align-items: center;
1160
+ justify-content: space-between;
1161
+ gap: var(--s2);
1162
+}
1163
+
1151
1164
/* hotkey help dialog */
1152
1165
1153
1166
dialog#hotkey-help {
@@ -1446,10 +1459,6 @@
1446
1459
flex: none;
1447
1460
}
1448
1461
1449
-.clone-url .copy-btn.copied {
1450
- color: var(--success-deep);
1451
-}
1452
-
1453
1462
#clone-https:checked ~ .url-https,
1454
1463
#clone-ssh:checked ~ .url-ssh {
1455
1464
display: flex;
MODIFY
src/main/resources/templates/SettingsResource/tokenCreated.html
+1 -1
@@ -2,7 +2,7 @@
2
2
{#title}Token created – git-shark{/title}
3
3
<h1>Token created</h1>
4
4
<p>Copy your new access token now — it will not be shown again:</p>
5
-<pre><code>{plaintext}</code></pre>
5
+<pre class="token-line"><code>{plaintext}</code><button type="button" class="btn-icon copy-btn" data-copy="{plaintext}" aria-label="Copy access token" title="Copy">⧉</button></pre>
6
6
<p>Use it as the password for Git over HTTPS.</p>
7
7
<p><a class="btn btn-secondary" href="/settings/tokens">Back to tokens</a></p>
8
8
{/include}
MODIFY
src/main/resources/templates/layout.html
+4 -0
@@ -13,10 +13,14 @@
13
13
<a class="brand" href="/"><svg class="bmark" viewBox="0 0 64 64" width="26" height="26" aria-hidden="true"><rect width="64" height="64" rx="15" fill="#168b85"></rect><path d="M12 42 H52 M24 42 C29 42 31 50 38 50" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"></path><circle cx="16" cy="42" r="3.2" fill="#fff"></circle><circle cx="38" cy="50" r="3.2" fill="#fff"></circle><path d="M26 42 C28 32 32 24 39 14 C40 22 42 33 45 42 Z" fill="#fff"></path></svg><span class="g">git</span><span class="s">shark</span></a>
14
14
<nav>
15
15
{#insert nav}
16
+ {#if cdi:currentUser.loggedIn}
16
17
<a href="/settings/profile">Profile</a>
17
18
<a href="/settings/keys">SSH keys</a>
18
19
<a href="/settings/tokens">Access tokens</a>
19
20
<form class="logout" method="post" action="/logout"><button type="submit">Logout</button></form>
21
+ {#else}
22
+ <a class="btn btn-primary" href="/login">Log in</a>
23
+ {/if}
20
24
{/}
21
25
</nav>
22
26
</header>
MODIFY
src/test/java/de/workaround/account/SettingsPagesTest.java
+13 -0
@@ -78,6 +78,19 @@
78
78
}
79
79
80
80
@Test
81
+ @TestSecurity(user = "token-copy-tester")
82
+ void tokenCreatedPageHasCopyButton()
83
+ {
84
+ given()
85
+ .formParam("label", "copy-token")
86
+ .when().post("/settings/tokens")
87
+ .then()
88
+ .statusCode(200)
89
+ .body(containsString("copy-btn"))
90
+ .body(containsString("data-copy=\"gs_"));
91
+ }
92
+
93
+ @Test
81
94
void anonymousIsRedirectedToLogin()
82
95
{
83
96
given()
MODIFY
src/test/java/de/workaround/web/DesignSystemTest.java
+10 -0
@@ -130,6 +130,16 @@
130
130
}
131
131
132
132
@Test
133
+ void copyButtonConfirmationStyleWorksOutsideCloneDialog()
134
+ {
135
+ // the copied-state color must apply to every [data-copy] button (e.g. the token page),
136
+ // not only to buttons inside the clone dialog
137
+ given().when().get("/shark.css")
138
+ .then().statusCode(200)
139
+ .body(containsString("\n.copy-btn.copied"));
140
+ }
141
+
142
+ @Test
133
143
void fontsAreSelfHostedWithoutCdnRequests()
134
144
{
135
145
given().when().get("/")
MODIFY
src/test/java/de/workaround/web/WebUiTest.java
+28 -0
@@ -500,6 +500,34 @@
500
500
}
501
501
502
502
@Test
503
+ void anonymousHeaderShowsOnlyLoginButton() throws Exception
504
+ {
505
+ given()
506
+ .when().get("/explore")
507
+ .then()
508
+ .statusCode(200)
509
+ .body(containsString("href=\"/login\""))
510
+ .body(not(containsString("Logout")))
511
+ .body(not(containsString("Access tokens")))
512
+ .body(not(containsString("/settings/profile")));
513
+ }
514
+
515
+ @Test
516
+ @TestSecurity(user = "ui-nav-user")
517
+ void loggedInHeaderShowsAccountNavigation() throws Exception
518
+ {
519
+ persistUser("ui-nav-user");
520
+
521
+ given()
522
+ .when().get("/explore")
523
+ .then()
524
+ .statusCode(200)
525
+ .body(containsString("Access tokens"))
526
+ .body(containsString("Logout"))
527
+ .body(not(containsString("href=\"/login\"")));
528
+ }
529
+
530
+ @Test
503
531
@TestSecurity(user = "ui-mallory")
504
532
void privateRepositoryHiddenFromStranger() throws Exception
505
533
{