Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Develop#101

Merged
Enes1998 merged 15 commits into
mainfrom
develop
Jun 13, 2026
Merged

Develop#101
Enes1998 merged 15 commits into
mainfrom
develop

Conversation

@Enes1998

Copy link
Copy Markdown
Collaborator

No description provided.

EdiAsllani and others added 15 commits June 10, 2026 13:02
…+ lock down /metrics

FR-4.7 says project data is invited-only, enforced backend, but three
project-scoped GET handlers only required authentication and filtered by
ProjectId — letting any signed-in user read another project's private
standups, standup CSV export, and commit history by guessing a project id:

- GetStandupFeedQueryHandler
- ExportStandupsCsvQueryHandler (previously only checked the project exists)
- GetProjectCommitsQueryHandler

Apply the same owner-or-accepted-member guard the sibling handlers
(GetProjectDashboard/GetProjectSummaries) already use, throwing
ForbiddenException (-> 403) for non-members. Document 403/404 on the
endpoints. Adds ProjectVisibilityGuardTests covering outsider/owner/member
for all three (6 tests).

Also closes public exposure of the Prometheus /metrics endpoint. The prod
nginx ingress has snippet directives disabled cluster-wide, so block by Host
in-app: MapMetrics("/metrics").RequireHost(Metrics:AllowedHosts). In-cluster
Prometheus scrapes the pod directly as 'loopless-backend:8080' (allow-listed)
while the public api host is not, returning 404 to the internet without
breaking scraping. Wired via Helm values config.backend.Metrics__AllowedHosts__0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-metrics-auth

fix(security): enforce project-member visibility + lock down /metrics
…nfig

Documents every NEXT_PUBLIC_* variable the frontend reads, the
window.__ENV runtime-injection mechanism (APP_* vars written to
public/__env.js by the container entrypoint), and which deployed
values live in devops/helm/loopless/values.yaml. Un-ignores
.env.example so it can be tracked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Message sends only broadcast over the MessagingHub, so the recipient never
got a bell notification unless they refreshed. Now SendMessageCommandHandler
also records an in-app notification (deduped to one unread entry per
conversation), and the RealtimeProvider suppresses it as read when the
recipient is already on the messages page.

Issue 2 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nosable and resilient

Support tickets and content flags already notify every admin, but delivery
depended entirely on a live SignalR connection. The bell now refetches on
open and on window focus as a fallback, and both handlers log how many
admins were notified (warning on zero, which means the admin exists only
in Keycloak, not the app DB).

Issue 3 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Dismiss/Warn/Suspend buttons swallowed every mutation failure, so a
failed review looked like a dead button. Failures now render an alert with
the API message, success shows a status line, and only the clicked row is
disabled while the request is in flight.

Issue 4 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The upload allowlist rejected .txt and .md (browsers send .md as
text/plain, often with no content type at all). Validation now treats the
extension allowlist as the mandatory check and tolerates empty or
octet-stream content types, while still excluding executable/markup types
(.html, .svg, .js, .exe). Adds xlsx/pptx for docx parity, sets the file
picker accept list, and fixes the frontend advertising 50 MB against the
backend 20 MB cap.

Issue 8 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suspension was a permanent boolean that nothing enforced — suspended users
could still log in. Adds User.SuspendedUntil (+migration), a duration
prompt (24h/7d/30d/indefinite) when suspending from the admin dashboard,
suspension checks on login and /me with lazy expiry, an hourly Hangfire
SuspensionExpiryJob sweep, and the until-date on the admin user row.

Issue 6 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The admin reset-password action sets the Keycloak UPDATE_PASSWORD required
action, but the login page had no way to satisfy it — the direct grant
failed with a generic invalid-credentials error. The Keycloak client now
distinguishes account-not-fully-set-up (credentials correct, action
pending) from a wrong password; login surfaces it as 409 with code
password_change_required; and a new rate-limited anonymous POST
/api/v1/auth/change-password verifies the old password, sets the new one,
clears the required action, and returns tokens. The login page switches to
an email + old/new/confirm password form and logs the user straight in.

Issue 5 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profile URLs exposed the user GUID. Users now get a unique URL-safe
username (slugified display name, GitHub handle for SSO signups, -2/-3 on
collisions, reserved route words excluded), generated at signup and
backfilled by migration for existing rows. The lookup endpoint accepts a
GUID or username — old links still resolve and the page canonicalizes the
address bar to /profile/{username}. Match cards link by username; own
profile links use /profile/me. The previously dead Share buttons on both
profile cards now copy the profile URL to the clipboard (with a
non-secure-context fallback) and confirm with Copied!.

Issue 7 of docs/Issues_2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Projects were GUID-routed end to end, exposing raw GUIDs in the address
bar (e.g. /projects/{guid}). Mirror the existing username system: generate
a stable, unique slug from the title at creation, route /projects/{slug},
and canonicalize legacy GUID links to the slug after load.

- Project.Slug column + unique index + backfill migration (AddProjectSlug)
- ProjectSlugGenerator: slugify title, reserve route words, suffix
  collisions with -2/-3 (slug is stable, never regenerated on rename)
- GET /projects/{idOrSlug} resolves a slug or a GUID; slug added to
  ProjectDto and DiscoverProjectDto
- Notification action URLs emit the slug instead of the GUID
- Frontend resolves the canonical project.id for all sub-resource calls,
  rewrites the address bar to the slug, and links by slug everywhere

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backend service never set Seed:Admin*, so docker-compose devs had no
admin account to exercise the admin panel (suspensions, forced password
resets, etc.). Add overridable Seed__Admin* env so AdminSeeder bootstraps
admin@loopless.local (Role=Admin) on startup. Defaults live in .env
(gitignored); log in at http://localhost:3002.

Also drops the logstash startup gate from the backend depends_on so the
API no longer blocks on logstash being up in local dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Enes1998 Enes1998 merged commit 24ea525 into main Jun 13, 2026
2 of 4 checks passed
Enes1998 added a commit that referenced this pull request Jun 13, 2026
develop changed GetProjectDashboardQuery to take string ProjectIdOrSlug (handler resolves guid-or-slug), but the GET /{id} and /{id}/dashboard endpoints still passed Guid -> CS1503 broke main build on merge #101. Relax the two routes to {projectIdOrSlug}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants