Working signout button
This commit is contained in:
@@ -3,6 +3,10 @@ APP_ENV=production
|
|||||||
APP_CONTAINER_NAME=linklog-app
|
APP_CONTAINER_NAME=linklog-app
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
APP_RESTART_POLICY=unless-stopped
|
APP_RESTART_POLICY=unless-stopped
|
||||||
|
APP_HEALTHCHECK_INTERVAL=30s
|
||||||
|
APP_HEALTHCHECK_TIMEOUT=5s
|
||||||
|
APP_HEALTHCHECK_START_PERIOD=10s
|
||||||
|
APP_HEALTHCHECK_RETRIES=3
|
||||||
LINKLOG_APP_NAME=LinkLog
|
LINKLOG_APP_NAME=LinkLog
|
||||||
LINKLOG_SECRET_KEY=replace-with-a-long-random-secret
|
LINKLOG_SECRET_KEY=replace-with-a-long-random-secret
|
||||||
LINKLOG_TOKEN_EXPIRY_DAYS=30
|
LINKLOG_TOKEN_EXPIRY_DAYS=30
|
||||||
@@ -18,6 +22,7 @@ TRAEFIK_HOST=localhost
|
|||||||
TRAEFIK_HTTP_PORT=80
|
TRAEFIK_HTTP_PORT=80
|
||||||
TRAEFIK_HTTP_INTERNAL_PORT=80
|
TRAEFIK_HTTP_INTERNAL_PORT=80
|
||||||
TRAEFIK_DASHBOARD_PORT=8080
|
TRAEFIK_DASHBOARD_PORT=8080
|
||||||
|
TRAEFIK_DASHBOARD_BIND_ADDRESS=127.0.0.1
|
||||||
TRAEFIK_API_INSECURE=true
|
TRAEFIK_API_INSECURE=true
|
||||||
TRAEFIK_RESTART_POLICY=unless-stopped
|
TRAEFIK_RESTART_POLICY=unless-stopped
|
||||||
DOCKER_SOCKET_PATH=/var/run/docker.sock
|
DOCKER_SOCKET_PATH=/var/run/docker.sock
|
||||||
|
|||||||
+11
@@ -10,7 +10,18 @@ RUN pip install --no-cache-dir -r backend/requirements.txt
|
|||||||
|
|
||||||
COPY backend ./backend
|
COPY backend ./backend
|
||||||
COPY frontend ./frontend
|
COPY frontend ./frontend
|
||||||
|
RUN useradd --create-home --uid 10001 linklog \
|
||||||
|
&& mkdir -p /app/backend/data \
|
||||||
|
&& chown -R linklog:linklog /app
|
||||||
|
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/health', timeout=3)"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ Appending a username to the root URL, such as `/alice`, opens that user's public
|
|||||||
|
|
||||||
Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user.
|
Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user.
|
||||||
|
|
||||||
|
Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is `PUT /api/user/password`.
|
||||||
|
|
||||||
On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: "` post prefix.
|
On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: "` post prefix.
|
||||||
|
|
||||||
## Run Tests
|
## Run Tests
|
||||||
@@ -131,6 +133,7 @@ The main configurable values are:
|
|||||||
| `TRAEFIK_HOST` | hostname routed by Traefik | `localhost` |
|
| `TRAEFIK_HOST` | hostname routed by Traefik | `localhost` |
|
||||||
| `TRAEFIK_HTTP_PORT` | host port for the application proxy | `80` |
|
| `TRAEFIK_HTTP_PORT` | host port for the application proxy | `80` |
|
||||||
| `TRAEFIK_DASHBOARD_PORT` | host port for the dashboard | `8080` |
|
| `TRAEFIK_DASHBOARD_PORT` | host port for the dashboard | `8080` |
|
||||||
|
| `TRAEFIK_DASHBOARD_BIND_ADDRESS` | host address for the dashboard | `127.0.0.1` |
|
||||||
| `TRAEFIK_API_INSECURE` | enable the local dashboard API | `true` |
|
| `TRAEFIK_API_INSECURE` | enable the local dashboard API | `true` |
|
||||||
| `APP_PORT` | direct host port for FastAPI | `8000` |
|
| `APP_PORT` | direct host port for FastAPI | `8000` |
|
||||||
|
|
||||||
@@ -149,6 +152,7 @@ The services are available at:
|
|||||||
- Traefik dashboard: <http://localhost:8080/dashboard/>
|
- Traefik dashboard: <http://localhost:8080/dashboard/>
|
||||||
|
|
||||||
The Compose configuration routes the hostname `localhost` through Traefik. The SQLite database is stored in the named Docker volume `linklog_data`, mounted at `/app/backend/data`.
|
The Compose configuration routes the hostname `localhost` through Traefik. The SQLite database is stored in the named Docker volume `linklog_data`, mounted at `/app/backend/data`.
|
||||||
|
The application runs as a non-root user and reports container health through `/health`; Traefik waits for the application health check before starting.
|
||||||
|
|
||||||
Stop the stack without deleting its database:
|
Stop the stack without deleting its database:
|
||||||
|
|
||||||
|
|||||||
@@ -271,3 +271,27 @@ Render edit action for owned links
|
|||||||
|
|
||||||
### Assistant outcome
|
### Assistant outcome
|
||||||
Verified that the feed renders the Edit button only when `is_owner` is true, while the backend owner check remains enforced by `PUT /api/links/{link_id}`. The live Docker-served feed asset contains the owner-gated action, focused tests passed, and the full strict suite passed with 12 tests.
|
Verified that the feed renders the Edit button only when `is_owner` is true, while the backend owner check remains enforced by `PUT /api/links/{link_id}`. The live Docker-served feed asset contains the owner-gated action, focused tests passed, and the full strict suite passed with 12 tests.
|
||||||
|
|
||||||
|
### User
|
||||||
|
On every page, when a user has authenticated show its avatar and name in the header next to a sign out putton.
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Added a shared authenticated header to the feed, login, admin, and profile pages. The shared script validates the stored token through `/api/auth/me`, displays the user's avatar and username beside `Sign out`, and shows `Sign in` when signed out. Uploaded avatars are used with an initial fallback. All pages include the shared header, the strict backend suite passed with 12 tests, and all browser scripts passed syntax checks.
|
||||||
|
|
||||||
|
### User
|
||||||
|
execute next todo
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Implemented the next unfinished requirements slice: authenticated password changes from the profile page. Added current-password verification, an 8-character minimum for new passwords, profile form controls, regression coverage, and README documentation. The strict backend suite passed with 12 tests.
|
||||||
|
|
||||||
|
### User
|
||||||
|
execute final todo (nummer 3)
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Completed the third remaining plan item by hardening Docker and Traefik: added non-root application execution, configurable health checks, health-gated Traefik startup, loopback dashboard binding, and a startup entrypoint that repairs ownership on existing SQLite volumes before dropping to UID 10001. Compose validation, a fresh image build, existing-volume startup, application health, and the strict backend suite all passed.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Pressing signout (on the home page) does not log the user out
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Found that the home page rendered the shared authenticated session header but did not load `logout.js`, so its sign-out button had no event handler. Added the logout handler to the home and login pages with cache-busted asset references. Verified real token revocation and the full strict backend suite with 12 passing tests.
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
44. Add authenticated link editing
|
44. Add authenticated link editing
|
||||||
45. Expose ownership if feed data
|
45. Expose ownership if feed data
|
||||||
46. Render edit action for owned links
|
46. Render edit action for owned links
|
||||||
|
47. On every page, when a user has authenticated show its avatar and name in the header next to a sign out putton.
|
||||||
|
48. execute next todo
|
||||||
|
49. execute final todo (nummer 3)
|
||||||
|
50. Pressing signout (on the home page) does not log the user out
|
||||||
|
|
||||||
## Future entries
|
## Future entries
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.app.api.dependencies import get_current_user
|
from backend.app.api.dependencies import get_current_user
|
||||||
from backend.app.database import AVATARS_DIR, get_connection
|
from backend.app.database import AVATARS_DIR, get_connection, hash_password
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -15,6 +15,11 @@ class UserConfigUpdate(BaseModel):
|
|||||||
bio: str | None = None
|
bio: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordUpdate(BaseModel):
|
||||||
|
current_password: str
|
||||||
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
class UserPluginConfigUpdate(BaseModel):
|
class UserPluginConfigUpdate(BaseModel):
|
||||||
instance: str | None = None
|
instance: str | None = None
|
||||||
access_token: str | None = None
|
access_token: str | None = None
|
||||||
@@ -60,6 +65,22 @@ def update_current_user_profile(
|
|||||||
return {'status': 'updated'}
|
return {'status': 'updated'}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/password')
|
||||||
|
def update_password(payload: PasswordUpdate, user: dict = Depends(get_current_user)):
|
||||||
|
if len(payload.new_password) < 8:
|
||||||
|
raise HTTPException(status_code=422, detail='New password must be at least 8 characters')
|
||||||
|
if hash_password(payload.current_password) != user['password_hash']:
|
||||||
|
raise HTTPException(status_code=400, detail='Current password is incorrect')
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
conn.execute(
|
||||||
|
'UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
(hash_password(payload.new_password), user['id']),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {'status': 'password_updated'}
|
||||||
|
|
||||||
|
|
||||||
@router.post('/avatar')
|
@router.post('/avatar')
|
||||||
async def upload_avatar(
|
async def upload_avatar(
|
||||||
avatar: UploadFile = File(...),
|
avatar: UploadFile = File(...),
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ def test_public_and_admin_pages_render_html():
|
|||||||
root_page = client.get('/')
|
root_page = client.get('/')
|
||||||
assert 'LinkLog' in root_page.text
|
assert 'LinkLog' in root_page.text
|
||||||
assert 'class="login-button" href="/login"' in root_page.text
|
assert 'class="login-button" href="/login"' in root_page.text
|
||||||
|
assert 'id="auth-session" class="auth-session hidden"' in root_page.text
|
||||||
|
assert 'logout.js?v=3' in root_page.text
|
||||||
assert client.get('/alice').status_code == 200
|
assert client.get('/alice').status_code == 200
|
||||||
assert client.get('/alice/').status_code == 200
|
assert client.get('/alice/').status_code == 200
|
||||||
user_page = client.get('/alice').text
|
user_page = client.get('/alice').text
|
||||||
@@ -182,14 +184,17 @@ def test_public_and_admin_pages_render_html():
|
|||||||
feed_script = TestClient(app).get('/static/feed.js?v=4').text
|
feed_script = TestClient(app).get('/static/feed.js?v=4').text
|
||||||
assert 'window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : \'/\')' in feed_script
|
assert 'window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : \'/\')' in feed_script
|
||||||
assert client.get('/login').status_code == 200
|
assert client.get('/login').status_code == 200
|
||||||
assert 'Sign in' in client.get('/login').text
|
login_page = client.get('/login').text
|
||||||
|
assert 'Sign in' in login_page
|
||||||
|
assert 'id="auth-session" class="auth-session hidden"' in login_page
|
||||||
|
assert 'logout.js?v=3' in login_page
|
||||||
assert client.get('/admin').status_code == 200
|
assert client.get('/admin').status_code == 200
|
||||||
admin_page = client.get('/admin').text
|
admin_page = client.get('/admin').text
|
||||||
assert 'Admin' in admin_page
|
assert 'Admin' in admin_page
|
||||||
assert 'id="admin-controls" class="hidden"' in admin_page
|
assert 'id="admin-controls" class="hidden"' in admin_page
|
||||||
assert 'id="admin-auth-notice" class="auth-notice hidden"' in admin_page
|
assert 'id="admin-auth-notice" class="auth-notice hidden"' in admin_page
|
||||||
assert 'id="admin-login-button" class="login-button" href="/login"' in admin_page
|
assert 'id="admin-login-button" class="login-button" href="/login"' in admin_page
|
||||||
assert 'id="logout-button" class="logout-button hidden"' in admin_page
|
assert 'id="auth-session" class="auth-session hidden"' in admin_page
|
||||||
feed_script = client.get('/static/feed.js?v=5').text
|
feed_script = client.get('/static/feed.js?v=5').text
|
||||||
assert 'if (item.is_owner)' in feed_script
|
assert 'if (item.is_owner)' in feed_script
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,26 @@ def test_user_config_api_and_profile_page():
|
|||||||
assert 'From my #LinkLog: "' in page_response.text
|
assert 'From my #LinkLog: "' in page_response.text
|
||||||
assert 'id="admin-link"' in page_response.text
|
assert 'id="admin-link"' in page_response.text
|
||||||
assert 'class="login-button hidden"' in page_response.text
|
assert 'class="login-button hidden"' in page_response.text
|
||||||
assert 'id="logout-button" class="logout-button hidden"' in page_response.text
|
assert 'id="auth-session" class="auth-session hidden"' in page_response.text
|
||||||
|
|
||||||
|
bob_login = client.post('/api/auth/login', json={
|
||||||
|
'username': 'bob',
|
||||||
|
'password': 'secret123',
|
||||||
|
}).json()
|
||||||
|
bob_headers = {'Authorization': f"Bearer {bob_login['access_token']}"}
|
||||||
|
password_response = client.put('/api/user/password', json={
|
||||||
|
'current_password': 'secret123',
|
||||||
|
'new_password': 'new-secret-123',
|
||||||
|
}, headers=bob_headers)
|
||||||
|
assert password_response.status_code == 200
|
||||||
|
assert client.post('/api/auth/login', json={
|
||||||
|
'username': 'bob',
|
||||||
|
'password': 'new-secret-123',
|
||||||
|
}).status_code == 200
|
||||||
|
assert client.put('/api/user/password', json={
|
||||||
|
'current_password': 'new-secret-123',
|
||||||
|
'new_password': 'secret123',
|
||||||
|
}, headers=bob_headers).status_code == 200
|
||||||
|
|
||||||
upload_response = client.post(
|
upload_response = client.post(
|
||||||
'/api/user/avatar',
|
'/api/user/avatar',
|
||||||
|
|||||||
+10
-1
@@ -16,6 +16,12 @@ services:
|
|||||||
LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30}
|
LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30}
|
||||||
LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-}
|
LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-}
|
||||||
restart: ${APP_RESTART_POLICY:-unless-stopped}
|
restart: ${APP_RESTART_POLICY:-unless-stopped}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/health', timeout=3)"]
|
||||||
|
interval: ${APP_HEALTHCHECK_INTERVAL:-30s}
|
||||||
|
timeout: ${APP_HEALTHCHECK_TIMEOUT:-5s}
|
||||||
|
start_period: ${APP_HEALTHCHECK_START_PERIOD:-10s}
|
||||||
|
retries: ${APP_HEALTHCHECK_RETRIES:-3}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.linklog.rule=Host(`${TRAEFIK_HOST:-localhost}`)"
|
- "traefik.http.routers.linklog.rule=Host(`${TRAEFIK_HOST:-localhost}`)"
|
||||||
@@ -31,8 +37,11 @@ services:
|
|||||||
- --api.insecure=${TRAEFIK_API_INSECURE:-true}
|
- --api.insecure=${TRAEFIK_API_INSECURE:-true}
|
||||||
ports:
|
ports:
|
||||||
- "${TRAEFIK_HTTP_PORT:-80}:${TRAEFIK_HTTP_INTERNAL_PORT:-80}"
|
- "${TRAEFIK_HTTP_PORT:-80}:${TRAEFIK_HTTP_INTERNAL_PORT:-80}"
|
||||||
- "${TRAEFIK_DASHBOARD_PORT:-8080}:8080"
|
- "${TRAEFIK_DASHBOARD_BIND_ADDRESS:-127.0.0.1}:${TRAEFIK_DASHBOARD_PORT:-8080}:8080"
|
||||||
restart: ${TRAEFIK_RESTART_POLICY:-unless-stopped}
|
restart: ${TRAEFIK_RESTART_POLICY:-unless-stopped}
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
|
- "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock:ro"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
chown -R linklog:linklog /app/backend/data
|
||||||
|
exec su -s /bin/sh linklog -c "exec $*"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
(() => {
|
||||||
|
const loginButton = document.querySelector('#auth-login-button');
|
||||||
|
const session = document.querySelector('#auth-session');
|
||||||
|
const avatar = document.querySelector('#auth-avatar');
|
||||||
|
const username = document.querySelector('#auth-username');
|
||||||
|
const token = localStorage.getItem('linklogAccessToken');
|
||||||
|
|
||||||
|
if (!loginButton || !session || !avatar || !username) return;
|
||||||
|
|
||||||
|
function showSignedOut() {
|
||||||
|
loginButton.classList.remove('hidden');
|
||||||
|
session.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSignedIn(user) {
|
||||||
|
loginButton.classList.add('hidden');
|
||||||
|
session.classList.remove('hidden');
|
||||||
|
username.textContent = user.username || '';
|
||||||
|
const initial = (user.username || 'U').slice(0, 1).toUpperCase();
|
||||||
|
avatar.textContent = initial;
|
||||||
|
if (user.avatar_url) {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.src = user.avatar_url;
|
||||||
|
image.alt = `${user.username || 'User'} avatar`;
|
||||||
|
image.addEventListener('error', () => {
|
||||||
|
image.remove();
|
||||||
|
avatar.textContent = initial;
|
||||||
|
});
|
||||||
|
avatar.textContent = '';
|
||||||
|
avatar.appendChild(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
showSignedOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/auth/me?token=${encodeURIComponent(token)}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error('Session expired');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(showSignedIn)
|
||||||
|
.catch(() => {
|
||||||
|
localStorage.removeItem('linklogAccessToken');
|
||||||
|
showSignedOut();
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const profileForm = document.querySelector('#profile-form');
|
const profileForm = document.querySelector('#profile-form');
|
||||||
|
const passwordForm = document.querySelector('#password-form');
|
||||||
const mastodonForm = document.querySelector('#mastodon-form');
|
const mastodonForm = document.querySelector('#mastodon-form');
|
||||||
const profileLogoutButton = document.querySelector('#logout-button');
|
const profileLogoutButton = document.querySelector('#logout-button');
|
||||||
const accessToken = localStorage.getItem('linklogAccessToken');
|
const accessToken = localStorage.getItem('linklogAccessToken');
|
||||||
@@ -90,6 +91,19 @@ mastodonForm.addEventListener('submit', async (event) => {
|
|||||||
setStatus('#mastodon-status', response.ok ? 'Mastodon settings saved.' : 'Could not save Mastodon settings.', !response.ok);
|
setStatus('#mastodon-status', response.ok ? 'Mastodon settings saved.' : 'Could not save Mastodon settings.', !response.ok);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
passwordForm.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const response = await fetch('/api/user/password', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: authHeaders(true),
|
||||||
|
body: JSON.stringify(Object.fromEntries(new FormData(passwordForm))),
|
||||||
|
});
|
||||||
|
const status = document.querySelector('#password-status');
|
||||||
|
status.textContent = response.ok ? 'Password changed.' : 'Could not change password.';
|
||||||
|
status.style.color = response.ok ? '#94e2d5' : '#f38ba8';
|
||||||
|
if (response.ok) passwordForm.reset();
|
||||||
|
});
|
||||||
|
|
||||||
Promise.all([loadProfile(), loadMastodonConfig()]).catch((error) => {
|
Promise.all([loadProfile(), loadMastodonConfig()]).catch((error) => {
|
||||||
setStatus('#profile-status', accessToken ? error.message : 'Please sign in first.', true);
|
setStatus('#profile-status', accessToken ? error.message : 'Please sign in first.', true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,6 +105,25 @@ body::selection {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-session {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-session .avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-basis: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-session .user-name {
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@@ -447,6 +466,10 @@ button:disabled {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-session {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<a id="admin-login-button" class="login-button" href="/login">Sign in</a>
|
<a id="admin-login-button" class="login-button" href="/login">Sign in</a>
|
||||||
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
|
<div id="auth-session" class="auth-session hidden">
|
||||||
|
<div id="auth-avatar" class="avatar"></div>
|
||||||
|
<span id="auth-username" class="user-name"></span>
|
||||||
|
<button id="logout-button" class="logout-button" type="button">Sign out</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +60,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<script src="/static/auth-header.js?v=1"></script>
|
||||||
<script src="/static/logout.js?v=2"></script>
|
<script src="/static/logout.js?v=2"></script>
|
||||||
<script src="/static/admin.js?v=2"></script>
|
<script src="/static/admin.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -14,7 +14,14 @@
|
|||||||
<h1>LinkLog</h1>
|
<h1>LinkLog</h1>
|
||||||
<p>Public link feed</p>
|
<p>Public link feed</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="login-button" href="/login">Sign in</a>
|
<div class="header-actions">
|
||||||
|
<a id="auth-login-button" class="login-button" href="/login">Sign in</a>
|
||||||
|
<div id="auth-session" class="auth-session hidden">
|
||||||
|
<div id="auth-avatar" class="avatar"></div>
|
||||||
|
<span id="auth-username" class="user-name"></span>
|
||||||
|
<button id="logout-button" class="logout-button" type="button">Sign out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -54,6 +61,8 @@
|
|||||||
<section id="feed" class="feed" aria-live="polite"></section>
|
<section id="feed" class="feed" aria-live="polite"></section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/static/auth-header.js?v=1"></script>
|
||||||
|
<script src="/static/logout.js?v=3"></script>
|
||||||
<script src="/static/feed.js?v=5"></script>
|
<script src="/static/feed.js?v=5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,8 +9,20 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Sign in</h1>
|
<div class="header-row">
|
||||||
<p>Access your LinkLog settings</p>
|
<div>
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
<p>Access your LinkLog settings</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a id="auth-login-button" class="login-button" href="/login">Sign in</a>
|
||||||
|
<div id="auth-session" class="auth-session hidden">
|
||||||
|
<div id="auth-avatar" class="avatar"></div>
|
||||||
|
<span id="auth-username" class="user-name"></span>
|
||||||
|
<button id="logout-button" class="logout-button" type="button">Sign out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
@@ -29,6 +41,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<script src="/static/auth-header.js?v=1"></script>
|
||||||
|
<script src="/static/logout.js?v=3"></script>
|
||||||
<script src="/static/login.js"></script>
|
<script src="/static/login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
<h1>Profile</h1>
|
<h1>Profile</h1>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<a id="admin-link" class="login-button hidden" href="/admin">Admin</a>
|
<a id="admin-link" class="login-button hidden" href="/admin">Admin</a>
|
||||||
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
|
<div id="auth-session" class="auth-session hidden">
|
||||||
|
<div id="auth-avatar" class="avatar"></div>
|
||||||
|
<span id="auth-username" class="user-name"></span>
|
||||||
|
<button id="logout-button" class="logout-button" type="button">Sign out</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +50,22 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="link-item settings-panel">
|
||||||
|
<h2>Password</h2>
|
||||||
|
<form id="password-form">
|
||||||
|
<label>
|
||||||
|
Current password
|
||||||
|
<input name="current_password" type="password" autocomplete="current-password" required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
New password
|
||||||
|
<input name="new_password" type="password" minlength="8" autocomplete="new-password" required />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Change password</button>
|
||||||
|
<p id="password-status" class="status" role="status"></p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="link-item settings-panel">
|
<section class="link-item settings-panel">
|
||||||
<h2>Mastodon</h2>
|
<h2>Mastodon</h2>
|
||||||
<form id="mastodon-form">
|
<form id="mastodon-form">
|
||||||
@@ -66,6 +86,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<script src="/static/auth-header.js?v=1"></script>
|
||||||
<script src="/static/logout.js?v=2"></script>
|
<script src="/static/logout.js?v=2"></script>
|
||||||
<script src="/static/profile.js?v=2"></script>
|
<script src="/static/profile.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user