gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ (web): Show only login in header for visitors; add token copy button

287d555d7b16042653069e60832c048bf17da191 · Michael Hainz · 2026-07-08T12:52:10Z

Changes

8 files changed, +81 -8

MODIFY README.md +2 -2
diff --git a/README.md b/README.md
index 0c97f99..9d59c60 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/main/java/de/workaround/account/CurrentUser.java b/src/main/java/de/workaround/account/CurrentUser.java
index 400e930..000607c 100644
--- a/src/main/java/de/workaround/account/CurrentUser.java
+++ b/src/main/java/de/workaround/account/CurrentUser.java
@@ -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
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index 5165b88..57c20a0 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -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
diff --git a/src/main/resources/templates/SettingsResource/tokenCreated.html b/src/main/resources/templates/SettingsResource/tokenCreated.html
index 493e2dd..e3f9acc 100644
--- a/src/main/resources/templates/SettingsResource/tokenCreated.html
+++ b/src/main/resources/templates/SettingsResource/tokenCreated.html
@@ -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
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index 488e351..9c370a5 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -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
diff --git a/src/test/java/de/workaround/account/SettingsPagesTest.java b/src/test/java/de/workaround/account/SettingsPagesTest.java
index 7e07917..ac8e5a7 100644
--- a/src/test/java/de/workaround/account/SettingsPagesTest.java
+++ b/src/test/java/de/workaround/account/SettingsPagesTest.java
@@ -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
diff --git a/src/test/java/de/workaround/web/DesignSystemTest.java b/src/test/java/de/workaround/web/DesignSystemTest.java
index 6ee1442..818baf4 100644
--- a/src/test/java/de/workaround/web/DesignSystemTest.java
+++ b/src/test/java/de/workaround/web/DesignSystemTest.java
@@ -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
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index 301a4d1..c955684 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -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 {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog