Commit Graph
261 Commits
Author SHA1 Message Date
kolaente 8f9b9aba68 feat(migration): index the columns that reference stored files
Adds indexes on task_attachments.file_id, users.avatar_file_id,
users.export_file_id, projects.background_file_id and
migration_status.upload_file_id.
2026-11-05 15:42:45 +01:00
kolaente 050c587ceb fix(user): only take username, name and language when creating a bot
Every other field of a new bot is set by the server.
2026-11-05 15:42:45 +01:00
kolaente dcf98317ca fix(user): build the TOTP replay key from the normalized passcode
Trim the passcode once, require exactly six digits and use the trimmed
code for both the check and the single-use key.
2026-11-05 15:42:45 +01:00
kolaenteandTink 859b5c5255 feat(user): notify users when an OIDC identity is linked 2026-11-05 12:44:50 +01:00
kolaenteandTink f0625e6d1c feat(api): add reauthenticated OIDC identity management endpoints 2026-11-05 12:44:50 +01:00
kolaenteandTink 934ced938e feat(auth): persist linked OIDC identities and authenticate local accounts 2026-11-05 12:44:49 +01:00
kolaenteandTink 61f8af603f refactor(user): share password and TOTP reauthentication 2026-11-05 12:44:49 +01:00
kolaente 28fa8b1525 fix(user): verify the authenticated account's password on email change
The password was checked through the login lookup by username or email, which could resolve to a different account, and the email of that account was then changed. Load the account by its ID and check its own password, keeping the re-authentication throttle and the lockout counter.
2026-11-03 22:03:18 +01:00
kolaente c5a8e3d8bb fix(user): count wrong passwords on password change toward account lockout
Checking the old password directly skipped the failed-attempt counter that login and the email-change prompt use. Extract CheckUserPasswordCountingFailures from CheckUserCredentials and use it for the password change too.
2026-11-03 20:15:14 +01:00
kolaente d58a4e0b21 fix(user): deliver password reset emails to locked accounts
ShouldNotify dropped every notification for locked accounts, including the reset email needed to get out of the lock. It now receives the notification and lets password resets through for locked accounts; disabled accounts, bots and all other notifications stay suppressed. The admin reset action no longer refuses locked accounts either.
2026-11-03 20:15:14 +01:00
kolaente ab745ee58a fix(user): bind email confirmation to the token and pending address it was issued for
Consume the exact confirmation token and only apply the update when the pending address is still the one that was read, so a concurrent email change cannot be confirmed by an older token.
2026-11-03 20:15:14 +01:00
kolaente c401aa0aa6 fix(user): match exact username search literally on PostgreSQL
ILIKE treated _ and \ in the search term as pattern characters, so the exact-username lookup could match other usernames. Compare case-insensitively with LOWER() instead.
2026-11-03 20:15:14 +01:00
kolaente 70b99a573a test(user): assert lockout event and notification 2026-11-03 10:41:13 +01:00
kolaente cda6d88330 feat(user): lock account after 10 failed password attempts
Mirrors the TOTP lockout: issue a password reset token, send a mail, set
the account to locked and revoke its sessions. Counts every wrong password
checked by CheckUserCredentials, including the password and email change
re-auth prompts, so a stolen session can no longer guess the password
indefinitely.

The 3-attempt warning mail no longer resets the counter, otherwise it
could never reach the lockout threshold.
2026-11-03 10:41:13 +01:00
kolaente 640ec3e2af fix(user): reset failed attempt counter after account lockout
Without this, the counter stays at 10 after a TOTP lockout, so the first
wrong passcode after a password reset locks the account again.
2026-11-03 10:41:13 +01:00
kolaente 27489e7724 refactor(user): extract account lockout from TOTP failure handling 2026-11-03 10:41:13 +01:00
kolaente 21cefe4d4a feat(user): attach bot owner to bot users in central loaders
GetUserByID and GetUsersByIDs now set BotOwner on bots, loading all
owners in one memoized query. Owners skip the disabled/locked gate so a
bot's creator stays discoverable after the owner is disabled.
2026-10-30 22:57:12 +01:00
kolaente 891d11da34 fix(avatar): keep the shared default avatar when replacing it
Address pr-swarm finding: users start on defaultsettings.avatar_file_id,
so the first avatar upload or provider sync deleted that shared file for
everyone. Replacing now skips it, through the same check account
deletion uses.
2026-10-20 23:05:30 +02:00
kolaente 3eb76fc774 fix(auth): reject bot credentials while the bot's owner can't authenticate 2026-10-20 15:13:02 +02:00
kolaente ec76dfbb74 refactor(user): use keyvalue.GetInt64 for failed login and TOTP counters
Address pr-swarm finding: both hand-rolled the int64/string parsing that
GetInt64 now does.
2026-10-20 15:13:02 +02:00
kolaente da41604b64 fix(auth): reject user JWTs whose session was revoked on the HTTP path 2026-10-20 15:13:02 +02:00
kolaente 866789ebad fix(user): enforce the email confirmation cooldown on redis
Redis returns integers as decimal strings, so the cooldown read 0 and was
never enforced. Add keyvalue.GetInt64 that reads either form.
2026-10-20 15:13:02 +02:00
kolaente a0454051cb fix(auth): refuse email-unconfirmed accounts on every credential path 2026-10-20 15:13:02 +02:00
kolaente fd3e77445f fix(totp): require the current password to enroll and enable totp 2026-10-20 15:13:02 +02:00
kolaente 9599ca2b0e fix(totp): require a current passcode to disable totp 2026-10-20 15:13:02 +02:00
kolaente 0b788b83d2 fix(user): throttle password re-checks per user on sensitive account actions 2026-10-20 15:13:02 +02:00
kolaente 07a4853876 fix(user): put account tokens in the email link fragment 2026-10-20 15:13:02 +02:00
Tyagiquamarandkolaente 576fef5738 fix(user): set default reminder settings on bot users and guard reminder parser (#4007) 2026-10-15 23:26:55 +02:00
kolaente a06dd4521e fix(user): revoke sessions when too many TOTP attempts lock the account
The auto-lock after ten failed TOTP attempts left sessions and sockets
alive until token expiry. Locking now dispatches an account-locked event
whose listener deletes the user's sessions.
2026-10-02 19:08:02 +02:00
kolaenteandTink 82a263cbe9 fix(caldav-tokens): honour page in the v2 token list
The handler returned every token on every page while reporting
total_pages, so paging through the list repeated the same rows.

Fixes #4001
2026-10-01 17:29:11 +02:00
kolaente f5c68ac049 fix(bots): 404 unowned bots, require status on PUT, audit owner status changes
- Owners can manage bots in any status (disabled or locked); unowned,
  missing and non-bot ids all return 404 so existence never leaks.
- PUT /api/v2/user/bots/{bot} requires status, so a partial body can no
  longer re-enable a disabled bot. A bot_status validator tag limits it
  to active/disabled.
- BotUser.Status is a pointer: the v1 update route keeps the current
  status when the body omits it instead of unlocking the bot.
- Owner status changes dispatch bot.status.changed for the audit log.
- Regenerate the frontend client and adapt the bot update callers.

Refs #3999
2026-10-01 15:27:34 +02:00
kolaente f6c8a2d479 test(user): enforce CalDAV token deletion ownership 2026-09-30 23:26:55 +02:00
kolaente 9313e94d83 test(user): reject tokens issued for another purpose 2026-09-30 23:26:55 +02:00
Mohd Quamar TyagiandGitHub 9dffe78277 fix(user): fallback empty user timezone to valid UTC location (#3965) (#3991) 2026-09-30 21:26:06 +00:00
kolaente d727c61f54 fix(user)!: give ErrCodeAccountLocked a unique error code
1026 was already taken by ErrorCodeUsernameReserved, which is older, so a
locked account was reported as a reserved username in clients.

BREAKING CHANGE: a locked account now returns 1040 instead of 1026.
2026-09-15 18:38:59 +02:00
kolaente 0336497d94 fix(user)!: give ErrCodeTOTPPasscodeUsed a unique error code
1025 was already taken by ErrorCodeInvalidTimezone, which is older, so
reusing a TOTP passcode (e.g. logging in right after enabling 2FA) showed
"The timezone '' is invalid." in the frontend.

Refs #3900

BREAKING CHANGE: a reused TOTP passcode now returns 1039 instead of 1025.
2026-09-15 18:38:59 +02:00
kolaente 27ccdba7a0 feat(invites): add admin-managed user invite links 2026-09-12 18:21:35 +02:00
kolaente 07e99dca0b test(db): cover the session memo
A behind-the-back write from a second session makes a memo hit the only way
the loader tests can pass.
2026-09-08 20:28:52 +02:00
kolaente 5baf839ded perf(db): memoize single-row task, project and user lookups per session
One GET /tasks/{id} loaded the task twice (Task.CanRead, then ReadOne), its
project twice and users by id several times, each a round trip.
GetTaskByIDSimple, GetProjectSimpleByID, GetProjectsMapByIDs,
user.GetUserByID and user.GetUsersByIDs now go through the per-session memo,
which drops itself as soon as that session writes anything.

db.Remember and db.RememberEach hold the get-or-fetch dance, mirroring
keyvalue.Remember; the two pre-existing memo sites in project_access.go use
them too. Callers get copies: Project.memoCopy re-allocates ParentProjectID,
and the user memo holds the raw row so the disabled/locked gate runs on
every hit rather than only on the first load.
2026-09-08 20:28:52 +02:00
kolaente 5ff724fe3e fix(user)!: move ErrCodeOpenIDCustomScopeMalformed off the username code
1022 was already taken by ErrCodeUsernameMustNotContainSpaces, which is
the older of the two, so a malformed OIDC scope rendered "The username
must not contain spaces." in clients.

BREAKING CHANGE: a malformed OIDC custom scope now returns 1038 instead
of 1022.
2026-09-06 00:12:30 +02:00
kolaente 50b68e1d3d fix(auth): hide enabled totp secrets 2026-08-31 18:37:03 +02:00
kolaente 170a5b7011 refactor(bots): take a verified user, not any web.Auth
Both helpers compared a.GetID() against users.id, but *LinkSharing also
satisfies web.Auth and its GetID() is a link_shares.id - a different id
namespace. Passing one would have emitted users.id = <share id> and lent
that share another user's identity, which is the confusion behind
GHSA-vvcv-vpph-h844. Guarded at every call site, but the helpers moved
out of models and into user, so the guard drifted away from the code
needing it. Taking *User makes the case unrepresentable.

Also fail closed on a zero id: humans store bot_owner_id = 0, so an
unguarded zero would have matched all of them.

The doc claimed JWT and CalDAV auth carry no BotOwnerID, but bots can
reach neither path, and claimed bots-cannot-own-bots as an invariant when
the guard is inert for JWT principals. Reworded to chain-safety by
construction, which holds regardless.
2026-08-29 23:29:46 +02:00
kolaente 86957c1bbd fix(labels): share labels between all bots of the same owner
The label access condition covered owner-to-bot and bot-to-owner, so two bots
of the same human could not use each other's labels: bot A seeded a label and
bot B got a 403 on it.

Replacing it with SameBotIdentityCond resolves every caller to an identity root
- a bot's owner, or a human themselves - and matches everyone under that root,
which covers all three directions in a single condition. Write access is
unchanged: only the creator and the human owner can update or delete a label.
2026-08-29 23:29:46 +02:00
kolaente 9c0fa73f3a feat(user): add shared bot identity helpers
IsBotOwnedBy and SameBotIdentityCond replace the ad-hoc bot ownership checks
scattered across labels and api tokens.
2026-08-29 23:29:46 +02:00
kolaente dc6365f0f4 fix(user): expire used TOTP passcodes via keyvalue TTL
Replaces the hand-rolled timestamp check and cleanup goroutine. The old
int64 type assertion never matched on redis (Get returns a string), so
passcode replay protection was silently disabled there.
2026-08-19 17:29:35 +02:00
kolaente bd704c347a feat(user): notify the current address when an email change is requested
Gives the account owner an out-of-band signal if someone with their
password starts moving the login address, with a link to the settings
page where the change can be cancelled. Sent after the confirmation mail
so a throttled request mails nothing.
2026-08-18 22:03:38 +02:00
kolaente 52bcd3e093 fix(user): keep old email active until new one is confirmed
Changing the email no longer overwrites the address and flips the account
to StatusEmailConfirmationRequired. The new address is stored as
pending_email and only applied when the confirm token is used (valid 24h),
so a lost or misaddressed confirmation mail can no longer lock the user
out. The change can be repeated to fix typos, cancelled, or the mail
resent; confirmation mails are throttled to one per minute per user, and
stale change tokens are swept by the cleanup cron.

Fixes #3481
2026-08-18 22:03:38 +02:00
kolaente 7677e70419 feat(user): add pending_email column
Stores a new address awaiting confirmation. A direct email update via
UpdateUser (admin, LDAP/OIDC sync) supersedes and clears it.
2026-08-18 22:03:38 +02:00
kolaente 3833b34d37 fix(user): keep email confirmation status after registration
The default-project creation triggered by the user-created event wrote the
stale in-memory user back with a full UpdateUser, racing registration and
sometimes resetting the status from "email confirmation required" back to
active. Only update the default_project_id column and keep the returned
user's status in sync with what was persisted.
2026-08-18 11:30:03 +02:00
kolaente ccba2e4546 fix: avoid panic on null jwt claim values
reflect.TypeOf returns a nil Type for a null claim value, so building the
error message panicked.
2026-08-04 13:40:11 +02:00