patx/gitman
Align header icons and move password changes to profile
Commit 00c32ec · patx · 2026-11-02T23:58:02-05:00
Comments
No comments yet.
Diff
diff --git a/app.py b/app.py
index 4ef38d2..7d94337 100644
--- a/app.py
+++ b/app.py
@@ -4995,7 +4995,7 @@ def change_password():
confirmation = request.forms.get("confirm_password", "")
def invalid(message, status=400):
response.status = status
- return render("edit_profile.tpl", profile=user, password_error=message)
+ return render_user_profile(user, password_error=message)
if len(current) > 1024 or not verify_password(current, user["password_hash"]):
record_auth_failure("password-change", identifier)
return invalid("Current password is incorrect.")
@@ -5017,7 +5017,7 @@ def change_password():
refreshed = get_user_by_id(user["id"])
request.environ["gitman.user"] = refreshed
login_user(refreshed)
- return render("edit_profile.tpl", profile=refreshed, notice="Password changed. Other browser sessions are signed out.")
+ return render_user_profile(refreshed, notice="Password changed. Other browser sessions are signed out.")
@app.route("/new", method=["GET", "POST"])
@@ -5041,6 +5041,10 @@ def user_profile(username):
profile_user = get_user_by_username(username.lower())
if not profile_user:
abort(404, "User not found.")
+ return render_user_profile(profile_user)
+
+
+def render_user_profile(profile_user, **context):
user = current_user()
active_tab = request.query.get("tab", "owned")
if active_tab not in {"owned", "stars"}:
@@ -5065,6 +5069,8 @@ def user_profile(username):
active_tab=active_tab,
is_self=bool(user and user["id"] == profile_user["id"]),
pagination=pagination_metadata(page, per_page, total=total),
+ current_url_with_page=lambda next_page: f"/{profile_user['username']}?" + urlencode({"tab": active_tab, "page": next_page}),
+ **context,
)
diff --git a/static/styles.css b/static/styles.css
index c2e0bd8..2230f2f 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -183,15 +183,19 @@ button, .button {
transform: scale(2);
transform-origin: center;
}
+.site-header, .nav { align-items: center; }
.nav-icons {
display: inline-flex;
align-items: center;
- color: black;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
font-weight: 700;
line-height: 1;
text-decoration: none;
color: var(--muted);
}
+.nav-icons svg { display: block; width: 24px; height: 24px; flex-shrink: 0; }
.nav form, .repo-tabs form, .inline-form { display: inline; }
.link-button { padding: 0; color: var(--link); background: none; border: 0; text-decoration: underline; }
.repo-tabs .repo-tab { padding-bottom: .2rem; border-bottom: 2px solid transparent; }
@@ -380,7 +384,6 @@ button, .button {
.grid.two, .commit-list li, .tag-list li { grid-template-columns: 1fr; }
}
-.profile-security { margin: 1.5rem 0; }
.password-dialog {
width: min(28rem, calc(100% - 2rem));
max-height: calc(100dvh - 2rem);
diff --git a/templates/base.tpl b/templates/base.tpl
index 1d83905..9f733e4 100644
--- a/templates/base.tpl
+++ b/templates/base.tpl
@@ -9,7 +9,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/static/git.svg">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)">
- <link rel="stylesheet" href="/static/styles.css?v=6">
+ <link rel="stylesheet" href="/static/styles.css?v=7">
</head>
<body>
<header class="site-header">
@@ -29,17 +29,17 @@
<path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2M8 1.918l-.797.161A4 4 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4 4 0 0 0-3.203-3.92zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5 5 0 0 1 13 6c0 .88.32 4.2 1.22 6"/>
</svg>
</a>
- <a class="nav-icons" href="/{{user['username']}}">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
- <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
- </svg>
- </a>
- <a class="nav-icons" href="/new">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-plus-square" viewBox="0 0 16 16">
+ <a class="nav-icons" href="/new" aria-label="New repository" title="New repository">
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-plus-square" viewBox="0 0 16 16" aria-hidden="true">
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
</svg>
</a>
+ <a class="nav-icons" href="/{{user['username']}}" aria-label="Profile" title="Profile">
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-person" viewBox="2 2 12 12" aria-hidden="true">
+ <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
+ </svg>
+ </a>
% else:
<a href="/login">Log in</a>
<a class="button small" href="/signup">Sign up</a>
diff --git a/templates/edit_profile.tpl b/templates/edit_profile.tpl
index d4ef85d..c04ae87 100644
--- a/templates/edit_profile.tpl
+++ b/templates/edit_profile.tpl
@@ -18,32 +18,5 @@
</label>
<button class="button" type="submit">Save profile</button>
</form>
- <p class="profile-security"><button type="button" id="change-password-open">Change password</button></p>
<p class="muted"><a href="/{{profile['username']}}"><- Back</a></p>
</section>
-
-<dialog id="password-dialog" class="password-dialog" aria-labelledby="password-title" data-reopen="{{'true' if get('password_error') else 'false'}}">
- <h2 id="password-title">Change password</h2>
- <form method="post" action="/settings/password">
- {{!csrf_field()}}
- <input type="text" name="username" value="{{user['username']}}" autocomplete="username" hidden>
- % if get('password_error'):
- <p class="alert" role="alert">{{password_error}}</p>
- % end
- <label>Current password
- <input type="password" name="current_password" autocomplete="current-password" maxlength="1024" required autofocus>
- </label>
- <label>New password
- <input type="password" name="new_password" autocomplete="new-password" minlength="8" maxlength="1024" required aria-describedby="password-hint">
- </label>
- <label>Confirm new password
- <input type="password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="1024" required>
- </label>
- <p class="muted" id="password-hint">At least 8 characters. Other browser sessions will be signed out.</p>
- <div class="password-actions">
- <button type="button" id="change-password-cancel">Cancel</button>
- <button type="submit">Change password</button>
- </div>
- </form>
-</dialog>
-<script src="/static/profile.js?v=1" defer></script>
diff --git a/templates/password_dialog.tpl b/templates/password_dialog.tpl
new file mode 100644
index 0000000..0b5da4c
--- /dev/null
+++ b/templates/password_dialog.tpl
@@ -0,0 +1,25 @@
+<dialog id="password-dialog" class="password-dialog" aria-labelledby="password-title" data-reopen="{{'true' if get('password_error') else 'false'}}">
+ <h2 id="password-title">Change password</h2>
+ <form method="post" action="/settings/password">
+ {{!csrf_field()}}
+ <input type="text" name="username" value="{{user['username']}}" autocomplete="username" hidden>
+ % if get('password_error'):
+ <p class="alert" role="alert">{{password_error}}</p>
+ % end
+ <label>Current password
+ <input type="password" name="current_password" autocomplete="current-password" maxlength="1024" required autofocus>
+ </label>
+ <label>New password
+ <input type="password" name="new_password" autocomplete="new-password" minlength="8" maxlength="1024" required aria-describedby="password-hint">
+ </label>
+ <label>Confirm new password
+ <input type="password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="1024" required>
+ </label>
+ <p class="muted" id="password-hint">At least 8 characters. Other browser sessions will be signed out.</p>
+ <div class="password-actions">
+ <button type="button" id="change-password-cancel">Cancel</button>
+ <button type="submit">Change password</button>
+ </div>
+ </form>
+</dialog>
+<script src="/static/profile.js?v=1" defer></script>
diff --git a/templates/profile.tpl b/templates/profile.tpl
index 1cfd365..25f1ef0 100644
--- a/templates/profile.tpl
+++ b/templates/profile.tpl
@@ -12,7 +12,7 @@
<p class="muted"><small>
Joined {{profile_user["created_at"]}}
% if is_self:
- <a class="button-link" href="/settings/profile">Edit profile</a> · <a class="button-link" href="/logout">Log out</a>
+ <a class="button-link" href="/settings/profile">Edit profile</a> · <button class="link-button" type="button" id="change-password-open">Change password</button> · <a class="button-link" href="/logout">Log out</a>
% end
</small></p>
% if profile_user["bio"]:
@@ -54,3 +54,7 @@
% include("pagination.tpl", pagination=pagination)
% end
</section>
+
+% if is_self:
+ % include("password_dialog.tpl")
+% end
diff --git a/tests/test_app.py b/tests/test_app.py
index d4c1199..2ad1776 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -2702,10 +2702,16 @@ def test_change_password_revokes_other_sessions_and_git_credentials(isolated_app
assert login_client(browser, 'alice').status_code == 303
legacy.cookies['session'] = cookie_encode(('session', str(owner['id'])), gitman.SECRET_KEY).decode()
assert legacy.get('/settings/profile').status_code == 200
- client.get('/settings/profile')
+ profile = client.get('/alice').text
+ assert 'id="change-password-open"' in profile
+ assert profile.index('aria-label="Notifications"') < profile.index('aria-label="New repository"') < profile.index('aria-label="Profile"')
+ assert 'id="change-password-open"' not in WsgiClient(isolated_app.app).get('/alice').text
+ assert 'id="change-password-open"' not in client.get('/settings/profile').text
result = client.post('/settings/password', {'current_password': 'correct horse battery staple',
'new_password': 'a different secure password', 'confirm_password': 'a different secure password'})
assert result.status_code == 200 and 'Password changed.' in result.text
+ assert 'id="change-password-open"' in result.text
+ assert '<h1>Edit profile</h1>' not in result.text
assert client.get('/settings/profile').status_code == 200
for browser in (other, legacy):
assert browser.get('/settings/profile').status_code == 303
@@ -2732,6 +2738,8 @@ def test_change_password_rejects_invalid_input_without_changing_credentials(isol
result = client.post('/settings/password', {'current_password':current, 'new_password':new, 'confirm_password':confirmation})
assert result.status_code == 400
assert 'data-reopen="true"' in result.text
+ assert 'id="change-password-open"' in result.text
+ assert '<h1>Edit profile</h1>' not in result.text
assert 'value="'+new+'"' not in result.text
assert isolated_app.get_user_by_id(owner['id'])['password_hash'] == owner['password_hash']