feat: user accounts — Google sign-in, DIY sessions, ownership scoping - #25
Conversation
🤖 CodeAnt AI — Review Status
|
✅ Deploy Preview for moderaty ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
User descriptionWhatMulti-user accounts so anyone can sign up, connect their YouTube channel, and use the tool — per the approved plan.
Verify
Deploy notesAfter merge: run migration 0004 on prod (additive, no data rewrite). First Google sign-in on the existing DB claims the orphaned channels — sign in with the owner account first. Add CodeAnt-AI DescriptionAdd Google accounts with private, ownership-scoped YouTube moderation What Changed
Impact
💡 Usage GuideChecking Your Pull RequestEvery time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later. Talking to CodeAnt AIGot a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask: This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code. ExamplePreserve Org Learnings with CodeAntYou can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input: This helps CodeAnt AI learn and adapt to your team's coding style and standards. ExampleRetrigger reviewAsk CodeAnt AI to review the PR again, by typing: Check Your Repository HealthTo analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health. |
There was a problem hiding this comment.
This PR implements comprehensive multi-user authentication with proper ownership scoping. The implementation includes DIY session management, Google OAuth integration, and ownership checks across all channel operations.
Critical Issue Found:
One security vulnerability requires immediate attention: missing foreign key constraint on sessions.userId that could allow deleted users to remain authenticated via unexpired sessions.
The ownership scoping implementation is thorough and correctly prevents cross-user access throughout the codebase. Session management follows secure practices with httpOnly cookies, sliding expiry, and proper CSRF protection via state tokens.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 2 high |
🔴 Metrics 89 complexity · 17 duplication
Metric Results Complexity ✅ 89 (≤ 100 complexity) Duplication ⚠️ 17 (≤ 1 duplication)
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
🏁 CodeAnt Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
PR Summary by QodoAdd Google sign-in accounts, custom sessions, and ownership-scoped channels
AI Description
Diagram
High-Level Assessment
Files changed (31)
|
PR Code Suggestions ✨Latest suggestions up to commit
|
| Category | Suggestion | Severity |
| Race condition |
Concurrent channel connections can bypass the ownership check and reassign a channelThe ownership check and the channel upsert are separate operations. Two users can src/routes/api/auth/google/callback/+server.ts [136-158] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/api/auth/google/callback/+server.ts
**Line:** 136:158
**Comment:**
*Race Condition: The ownership check and the channel upsert are separate operations. Two users can concurrently observe no conflicting owner, after which the later `onConflictDoUpdate` overwrites the existing owner and encrypted refresh token. Make the ownership condition part of an atomic conditional update or enforce the conflict inside a transaction so a channel cannot be reassigned after the check.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Critical |
| Security |
Returning the full channel row exposes the encrypted refresh credential in browser load dataThe channel query uses an unrestricted src/routes/(app)/channels/[id]/queue/+page.server.ts [43-49] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/(app)/channels/[id]/queue/+page.server.ts
**Line:** 43:49
**Comment:**
*Security: The channel query uses an unrestricted `select()` and the returned row is included in the server load result. SvelteKit serializes this result to the browser, so the encrypted YouTube refresh token in `ch.refreshTokenEnc` is exposed to the owning user's client. Project only the channel fields needed by the queue page, excluding credential columns.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major |
The rules load exposes the channel's encrypted refresh credential to the browserThe rules page returns the full channel row from its server load, including src/routes/(app)/channels/[id]/rules/+page.server.ts [29-35] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/(app)/channels/[id]/rules/+page.server.ts
**Line:** 29:35
**Comment:**
*Security: The rules page returns the full channel row from its server load, including `refreshTokenEnc`, which SvelteKit serializes to the browser. This route should use an explicit non-secret projection rather than returning the database row.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major | |
Every newly created account can claim all remaining orphaned channelsThe orphan-claim update runs for every newly created Google account, not only for src/routes/api/auth/google/login/callback/+server.ts [118-128] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/api/auth/google/login/callback/+server.ts
**Line:** 118:128
**Comment:**
*Security: The orphan-claim update runs for every newly created Google account, not only for the first account. Any remaining nullable channel owner is therefore assigned wholesale to whichever unrelated user signs in next, causing ownership and access-control corruption. Gate this migration claim to the first-account condition or perform an explicit one-time migration.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major | |
| Possible bug |
Userinfo network failures escape as unhandled errors instead of controlled sign-in failuresThe userinfo request and src/routes/api/auth/google/login/callback/+server.ts [93-97] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/api/auth/google/login/callback/+server.ts
**Line:** 93:97
**Comment:**
*Possible Bug: The userinfo request and `infoRes.text()` are outside the error handler used for the token exchange. Network failures and timeout exceptions from either operation escape as unhandled route errors instead of the documented retryable 502 response. Wrap the complete userinfo request and body read in equivalent error handling.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major |
There was a problem hiding this comment.
Pull Request Overview
The PR successfully implements Google identity integration, DIY session management, and strict ownership scoping for channel data. Functional acceptance criteria, including the 404 response for cross-owner access and the 409 conflict for existing channel connections, appear well-handled.
However, the overall code quality is not up to standards. The primary concern is the significant duplication of logic for verifying channel ownership and performing OAuth token exchanges. Because these are security-critical paths, they must be refactored into shared utilities to prevent inconsistent enforcement of access controls in the future. Additionally, there is minor clutter in the TypeScript definitions and duplication in the test utility helpers.
About this PR
- The PR exhibits a pattern of duplicating sensitive logic, specifically around channel ownership verification and Google OAuth token exchange. Centralizing these into shared server-side utilities is recommended to ensure consistency and simplify future maintenance of the authentication and authorization layers.
1 comment outside of the diff
src/app.d.ts
line 18⚪ LOW RISK
Nitpick: This empty export is unnecessary. Since this file already imports SessionUser, theexport {}is redundant and can be safely removed.
Test suggestions
- DIY session lifecycle: creation, resolution, renewal within the 15-day window, and destruction.
- Google identity sign-in flow: CSRF state verification, token exchange, and find-or-create user logic.
- Ownership scoping: The dashboard must only display channels belonging to the authenticated user.
- Cross-owner protection: Form actions (queue, rules, settings) must return 404 when targeting another user's channel.
- Authentication guard: Form actions must return 401 when the session is missing or expired.
- Conflict prevention: Connecting a YouTube channel already linked to a different account must return 409.
- Migration logic: Orphaned channels are correctly claimed by the first user who registers.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| import { createSession, SESSION_COOKIE } from '$lib/server/session'; | ||
|
|
||
| export async function GET({ url, cookies }: { url: URL; cookies: import('@sveltejs/kit').Cookies }) { | ||
| const state = url.searchParams.get('state'); |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Suggestion: The logic for exchanging an authorization code for tokens and fetching Google user information is duplicated between the identity callback and the YouTube connection callback. Extracting this into a common service (e.g., src/lib/server/google.ts) would ensure consistent error handling for upstream failures.
Code Review by Qodo
Context used✅ Compliance rules (platform):
51 rules 1.
|
| /** | ||
| * Resolves a session cookie token to its user, or null when unknown or | ||
| * expired. Expired rows are deleted lazily on read; sessions in their last 15 | ||
| * days are renewed in place (sliding expiry) and reported via `renewed` so | ||
| * the caller can refresh the cookie. | ||
| */ | ||
| export async function getSessionUser(token: string | undefined): Promise<SessionResolution | null> { | ||
| if (!token) return null; | ||
| const row = await db | ||
| .select({ session: sessions, user: { id: users.id, email: users.email, displayName: users.displayName, plan: users.plan } }) | ||
| .from(sessions) | ||
| .innerJoin(users, eq(sessions.userId, users.id)) | ||
| .where(eq(sessions.id, token)) | ||
| .get(); | ||
| if (!row) return null; | ||
| const expiresMs = Date.parse(row.session.expiresAt); | ||
| if (Number.isNaN(expiresMs) || expiresMs <= Date.now()) { | ||
| await db.delete(sessions).where(eq(sessions.id, token)); | ||
| return null; | ||
| } | ||
| if (expiresMs - Date.now() < RENEW_BELOW_MS) { | ||
| const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString(); | ||
| await db.update(sessions).set({ expiresAt }).where(eq(sessions.id, token)); | ||
| return { user: row.user, expiresAt, renewed: true }; | ||
| } | ||
| return { user: row.user, expiresAt: row.session.expiresAt, renewed: false }; | ||
| } | ||
|
|
||
| /** Deletes a session (sign-out). Unknown tokens are a no-op. */ | ||
| export async function destroySession(token: string): Promise<void> { | ||
| await db.delete(sessions).where(eq(sessions.id, token)); | ||
| } |
There was a problem hiding this comment.
4. Expired sessions never purged 🐞 Bug ☼ Reliability
Expired session rows are only deleted when that exact token is looked up, so sessions for users who stop using the app remain in the sessions table indefinitely. Over time this can bloat the DB and increase maintenance/backup costs.
Agent Prompt
### Issue description
`getSessionUser()` only deletes an expired session row when a request presents that specific token; there is no bulk cleanup path for sessions that expire while the user is inactive.
### Issue Context
This PR introduces a `sessions` table and sliding-expiry cookie sessions.
### Fix Focus Areas
- src/lib/server/session.ts[56-87]
### Suggested fix approach
Implement bounded cleanup of expired sessions, for example:
- Add a helper like `purgeExpiredSessions(nowIso)` that does `DELETE FROM sessions WHERE expires_at <= nowIso`.
- Call it opportunistically (e.g., on `createSession()`), or incorporate it into an existing scheduled pathway (e.g., the existing `/api/cron` handler) so cleanup is guaranteed even for inactive users.
- If you expect many rows, consider limiting frequency (e.g., probabilistic/once-per-hour guard) to avoid doing a full-table delete too often.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@drizzle/0004_smiling_nextwave.sql`:
- Around line 19-36: The sessions table in the migration lacks referential
integrity and index support. Update the sessions table definition in schema.ts
to add a FOREIGN KEY constraint on the user_id column referencing users(id),
then add an index on sessions.user_id for query performance. Additionally, add
an index on sessions.expires_at if your expiry cleanup queries filter or sort by
that column. Regenerate the migration file to reflect these schema changes.
In `@EXECUTION_PLAN_YouTube_Comment_Moderator.md`:
- Around line 1887-1888: Update the Google sign-in documentation to use the
complete OAuth callback path `/api/auth/google/login/callback`, alongside
`/api/auth/google/login`, so the documented redirect URI matches the route
implemented by the callback server.
In `@src/hooks.server.ts`:
- Around line 26-40: Wrap the getSessionUser call in handle with server-side
error handling so database failures are caught and logged instead of propagating
through every request. On failure, set event.locals.user to null and skip
session-renewal logic; preserve the existing successful resolution and renewal
behavior.
In `@src/lib/server/db/schema.ts`:
- Around line 31-40: Add foreign key references from sessions.userId and
channels.userId to users.id in the sqliteTable definitions, using onDelete:
'restrict' rather than cascade. Preserve channels.userId as nullable so orphaned
pre-account channels remain reclaimable through the existing null path.
In `@src/lib/server/session.ts`:
- Around line 62-82: Add a periodic session-cleanup job alongside the existing
cron or lease mechanisms that deletes all rows from sessions whose expiresAt is
earlier than the current time, rather than relying only on getSessionUser. Reuse
the existing database access and scheduling patterns, and keep getSessionUser’s
per-token expiry handling unchanged.
In `@src/routes/`(app)/channels/[id]/queue/+page.server.ts:
- Around line 40-50: Extract the duplicated channel-ownership authorization into
one shared ownedChannel helper, retaining the canonical implementation in
src/routes/(app)/channels/[id]/queue/+page.server.ts#L40-L50 or moving it to a
shared module and importing it. Delete the local ownedChannel definition from
src/routes/(app)/channels/[id]/rules/+page.server.ts#L26-L36 and import the
shared helper; replace the inlined ownership check in
src/routes/(app)/channels/[id]/log/+page.server.ts#L21-L33 with a call to
ownedChannel, preserving the existing requireUser, scoped query, and 404
behavior.
In `@src/routes/api/auth/google/callback/`+server.ts:
- Around line 134-158: The upsert in the channels insert block has a race
condition where the separate ownership check can be bypassed by concurrent
requests. Replace the unconditional onConflictDoUpdate with a setWhere clause
that ensures updates only occur when the existing channel is owned by user.id or
is new, add .returning() to detect when no rows are updated, and throw a 409
error if the update was skipped (indicating another account now owns the
channel). Import sql from the drizzle library to construct the predicate if
needed, and remove or simplify the separate SELECT query if it is no longer
necessary for your requirements.
In `@src/routes/api/auth/google/login/callback/`+server.ts:
- Around line 39-113: Extract the shared token-exchange, JSON-parsing, and
redacted-error-logging sequence into a reusable helper function named
exchangeGoogleAuthCode in $lib/server that accepts code and redirectUri as
parameters. In src/routes/api/auth/google/login/callback/+server.ts lines
39-113, replace the entire token-exchange and error-handling block (including
the nested-ternary error-detail construction at line 83) with a call to this new
helper, keeping only the userinfo lookup and identity validation that follow. In
src/routes/api/auth/google/callback/+server.ts lines 42-131, apply the same
helper to the equivalent token-exchange section, then layer the channel-specific
refresh_token requirement and YouTube channel lookup logic on top of the
helper's result. This consolidates the duplicated validation and error handling
while allowing each route to add its own business logic.
- Around line 117-128: Update the find-or-create flow around the users lookup
and insert to catch a unique-constraint failure on users.googleSub
(users_google_sub_unique), then re-select the existing user by info.sub and
continue normally. Preserve the existing insert and orphan-channel claiming
behavior for the callback that successfully creates the user, while surfacing
unrelated database errors unchanged.
In `@src/routes/api/auth/google/login/login.test.ts`:
- Around line 43-67: The test helper functions makeCookies,
makeCookiesWithState, and callbackUrl are duplicated across test files. Create a
new shared test utility module (e.g., a common helpers file for Google auth
tests) and move these three function definitions there. In
src/routes/api/auth/google/login/login.test.ts (lines 43-67), remove the
duplicate function definitions and import them from the shared utility module
instead. In src/routes/api/auth/google/oauth.test.ts (lines 58-83), do the same:
remove the duplicate makeCookies and makeCookiesWithState definitions and import
them from the same shared utility module.
In `@src/routes/login/`+page.svelte:
- Around line 25-51: Update the login page component around the populated login
card to render loading, empty, error, and populated states, using a skeleton for
loading, the shared EmptyState for empty results, and an `.error-box` for
authentication failures. Ensure route authentication errors select the error
state instead of the populated card, and add a route or component test covering
all required states.
In `@src/routes/logout/`+page.server.ts:
- Around line 29-35: Update the default action in actions to accept locals and
call requireUser(locals) before destroySession(token), ensuring unauthenticated
POST requests cannot delete sessions or complete logout. Add an action test
covering an unauthenticated POST and verifying the session deletion call is not
reached.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 39a1dcb8-5ef8-4b5b-8e5d-d342b63033a5
📒 Files selected for processing (31)
AGENTS.mdEXECUTION_PLAN_YouTube_Comment_Moderator.mdREADME.mddrizzle/0004_smiling_nextwave.sqldrizzle/meta/0004_snapshot.jsondrizzle/meta/_journal.jsonsrc/app.d.tssrc/hooks.server.tssrc/lib/server/db/schema.tssrc/lib/server/session.test.tssrc/lib/server/session.tssrc/lib/server/testdb.tssrc/routes/(app)/+layout.server.tssrc/routes/(app)/+layout.sveltesrc/routes/(app)/channels/[id]/log/+page.server.tssrc/routes/(app)/channels/[id]/queue/+page.server.tssrc/routes/(app)/channels/[id]/queue/actions.test.tssrc/routes/(app)/channels/[id]/rules/+page.server.tssrc/routes/(app)/channels/[id]/rules/actions.test.tssrc/routes/(app)/dashboard/+page.server.tssrc/routes/(app)/dashboard/actions.test.tssrc/routes/(app)/dashboard/dashboard.test.tssrc/routes/+page.sveltesrc/routes/api/auth/google/callback/+server.tssrc/routes/api/auth/google/login/+server.tssrc/routes/api/auth/google/login/callback/+server.tssrc/routes/api/auth/google/login/login.test.tssrc/routes/api/auth/google/oauth.test.tssrc/routes/login/+page.server.tssrc/routes/login/+page.sveltesrc/routes/logout/+page.server.ts
| export const sessions = sqliteTable('sessions', { | ||
| id: text('id').primaryKey(), // random 32-byte hex token; also the cookie value | ||
| userId: text('user_id').notNull(), | ||
| expiresAt: text('expires_at').notNull(), // ISO timestamp; sliding 30-day expiry | ||
| createdAt: text('created_at').notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) | ||
| }); | ||
|
|
||
| export const channels = sqliteTable('channels', { | ||
| id: text('id').primaryKey(), // YouTube channel ID (UC...) | ||
| userId: text('user_id'), // owning user; null = pre-accounts orphan, claimed on first login |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Is drizzle-kit issue #5782 (SQLite ON DELETE CASCADE table-rebuild data loss) fixed, and in which version?
💡 Result:
Drizzle-kit issue #5782, which concerns silent data loss in SQLite during table-rebuild migrations involving ON DELETE CASCADE foreign keys, was addressed by Pull Request #5784 [1][2]. The PR introduces a fix that prevents this silent data loss by properly managing the PRAGMA foreign_keys setting at the session level, ensuring that foreign key enforcement is reliably disabled during the table-rebuild process [2]. As of July 31, 2026, this issue is considered resolved by PR #5784. Users experiencing this behavior should ensure they are using a version of drizzle-kit that includes this fix [2]. While the exact version number containing the fix was not explicitly identified in the provided sources, the fix was merged to address the specific mechanism of silent deletion described in issue #5782 [1][2]. Users are encouraged to update to the latest available version of drizzle-kit to benefit from this improvement.
Citations:
- 1: [BUG]: SQLite table-rebuild migrations silently wipe child tables for any FK with
ON DELETE CASCADEdrizzle-team/drizzle-orm#5782 - 2: fix(sqlite): prevent silent ON DELETE CASCADE data loss during table-rebuild migrations drizzle-team/drizzle-orm#5784
🏁 Script executed:
# Find package.json and check drizzle-kit version
find . -name "package.json" -type f | head -5 | xargs grep -l "drizzle" 2>/dev/null || true
# Check for lock files that might have version info
if [ -f "package.json" ]; then
cat package.json | grep -A 2 -B 2 "drizzle"
fi
if [ -f "package-lock.json" ]; then
rg "\"drizzle-kit\"" package-lock.json -A 5 | head -30
fi
if [ -f "yarn.lock" ]; then
rg "drizzle-kit@" yarn.lock -A 3 | head -30
fi
if [ -f "pnpm-lock.yaml" ]; then
rg "drizzle-kit:" pnpm-lock.yaml -A 3 | head -30
fiRepository: Bonobo791/Moderaty
Length of output: 2023
🌐 Web query:
drizzle-kit PR #5784 merged date and first release version containing the fix
💡 Result:
Drizzle-team pull request #5784, which addresses SQLite table-rebuild migrations causing silent data loss for ON DELETE CASCADE foreign keys, was created on May 19, 2026 [1]. As of July 31, 2026, the pull request remains in an open state [1]. Consequently, because the PR has not been merged, it is not included in any release version [1].
Citations:
Add foreign key references for sessions.userId and channels.userId.
Neither sessions.userId (Line 33) nor channels.userId (Line 40) references users.id. Without a foreign key, the database allows a row to point at a user that no longer exists. When getSessionUser performs an innerJoin on users (src/lib/server/session.ts Line 67), an orphaned session simply stops resolving, but its row is never deleted because the lazy-delete path only runs on the expiry branch, not when a join match fails. An orphaned channels.userId similarly falls outside the userId IS NULL reclaim path documented in the inline comment, leaving a permanently stuck row.
No code in this PR deletes a users row yet, so this is not exploitable today. Add the reference now, before more features depend on the current ownership model.
Do not use onDelete: 'cascade'. Drizzle-kit issue #5782 causes SQLite table-rebuild migrations to silently delete rows in child tables that reference the rebuilt table through ON DELETE CASCADE. Pull Request #5784 addresses this issue but remains unmerged and is not available in any released version of drizzle-kit as of July 2026. Until PR #5784 is merged and released, use onDelete: 'restrict' instead, or implement cleanup in application code. This prevents data loss during unrelated schema changes to the users table.
Proposed schema change
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(), // random 32-byte hex token; also the cookie value
- userId: text('user_id').notNull(),
+ userId: text('user_id').notNull().references(() => users.id, { onDelete: 'restrict' }),
expiresAt: text('expires_at').notNull(), // ISO timestamp; sliding 30-day expiry
createdAt: text('created_at').notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`)
});
export const channels = sqliteTable('channels', {
id: text('id').primaryKey(), // YouTube channel ID (UC...)
- userId: text('user_id'), // owning user; null = pre-accounts orphan, claimed on first login
+ userId: text('user_id').references(() => users.id, { onDelete: 'set null' }), // owning user; null = pre-accounts orphan, claimed on first login📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const sessions = sqliteTable('sessions', { | |
| id: text('id').primaryKey(), // random 32-byte hex token; also the cookie value | |
| userId: text('user_id').notNull(), | |
| expiresAt: text('expires_at').notNull(), // ISO timestamp; sliding 30-day expiry | |
| createdAt: text('created_at').notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) | |
| }); | |
| export const channels = sqliteTable('channels', { | |
| id: text('id').primaryKey(), // YouTube channel ID (UC...) | |
| userId: text('user_id'), // owning user; null = pre-accounts orphan, claimed on first login | |
| export const sessions = sqliteTable('sessions', { | |
| id: text('id').primaryKey(), // random 32-byte hex token; also the cookie value | |
| userId: text('user_id').notNull().references(() => users.id, { onDelete: 'restrict' }), | |
| expiresAt: text('expires_at').notNull(), // ISO timestamp; sliding 30-day expiry | |
| createdAt: text('created_at').notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) | |
| }); | |
| export const channels = sqliteTable('channels', { | |
| id: text('id').primaryKey(), // YouTube channel ID (UC...) | |
| userId: text('user_id').references(() => users.id, { onDelete: 'set null' }), // owning user; null = pre-accounts orphan, claimed on first login |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/server/db/schema.ts` around lines 31 - 40, Add foreign key references
from sessions.userId and channels.userId to users.id in the sqliteTable
definitions, using onDelete: 'restrict' rather than cascade. Preserve
channels.userId as nullable so orphaned pre-account channels remain reclaimable
through the existing null path.
| export async function getSessionUser(token: string | undefined): Promise<SessionResolution | null> { | ||
| if (!token) return null; | ||
| const row = await db | ||
| .select({ session: sessions, user: { id: users.id, email: users.email, displayName: users.displayName, plan: users.plan } }) | ||
| .from(sessions) | ||
| .innerJoin(users, eq(sessions.userId, users.id)) | ||
| .where(eq(sessions.id, token)) | ||
| .get(); | ||
| if (!row) return null; | ||
| const expiresMs = Date.parse(row.session.expiresAt); | ||
| if (Number.isNaN(expiresMs) || expiresMs <= Date.now()) { | ||
| await db.delete(sessions).where(eq(sessions.id, token)); | ||
| return null; | ||
| } | ||
| if (expiresMs - Date.now() < RENEW_BELOW_MS) { | ||
| const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString(); | ||
| await db.update(sessions).set({ expiresAt }).where(eq(sessions.id, token)); | ||
| return { user: row.user, expiresAt, renewed: true }; | ||
| } | ||
| return { user: row.user, expiresAt: row.session.expiresAt, renewed: false }; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider a periodic sweep for stale sessions.
getSessionUser only deletes an expired row when someone presents that exact token again. A session belonging to a user who never returns after expiry stays in the sessions table indefinitely. Add a scheduled cleanup job (for example, alongside the existing cron/lease mechanisms) that deletes rows where expiresAt is in the past.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/server/session.ts` around lines 62 - 82, Add a periodic
session-cleanup job alongside the existing cron or lease mechanisms that deletes
all rows from sessions whose expiresAt is earlier than the current time, rather
than relying only on getSessionUser. Reuse the existing database access and
scheduling patterns, and keep getSessionUser’s per-token expiry handling
unchanged.
| <main class="login-main"> | ||
| <div class="card login-card"> | ||
| <h1>Sign in to Moderaty</h1> | ||
| <p class="muted"> | ||
| Sign in with your Google account, then connect your YouTube channel to start moderating | ||
| comments automatically. | ||
| </p> | ||
| <a class="btn" href="/api/auth/google/login">Sign in with Google</a> | ||
| </div> | ||
| </main> | ||
|
|
||
| <style> | ||
| .login-main { | ||
| display: grid; | ||
| place-items: center; | ||
| min-height: 100vh; | ||
| padding: 24px; | ||
| } | ||
| .login-card { | ||
| max-width: 420px; | ||
| text-align: center; | ||
| } | ||
| .login-card .btn { | ||
| display: inline-block; | ||
| margin-top: 16px; | ||
| } | ||
| </style> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Render the required login page states.
This page renders only the populated login card. Add a loading skeleton, an EmptyState, and an .error-box. Route authentication failures to the .error-box.
Add a route or component test that fails until each required state renders.
As per coding guidelines, “Every page must implement loading with a skeleton, an EmptyState empty view, an .error-box error view, and a populated view.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/login/`+page.svelte around lines 25 - 51, Update the login page
component around the populated login card to render loading, empty, error, and
populated states, using a skeleton for loading, the shared EmptyState for empty
results, and an `.error-box` for authentication failures. Ensure route
authentication errors select the error state instead of the populated card, and
add a route or component test covering all required states.
Source: Coding guidelines
…hange/ownership/cookie helpers
|
Review round 1 — resolved in c7cbf1a (160/160 tests, check 0 errors, build green): amazon-q — missing FK on `sessions.userId`: valid, fixed. `sessions.userId` now `REFERENCES users(id) ON DELETE CASCADE` with a `sessions_user_id_idx` index. Since migration 0004 was already applied to the prod Turso (empty tables, feature unmerged), this ships as a new migration 0005 (SQLite table-rebuild + index) — 0004 stays immutable. 0005 has been applied to prod and verified (FK + index present). codacy — duplicated token-exchange logic in the two OAuth callbacks: valid, fixed. Extracted `exchangeGoogleCode()` into `src/lib/server/google.ts`: one implementation of the one-time-code exchange with redacted-error logging and response validation; the login and connect callbacks keep only their flow-specific parts (refresh_token requirement, userinfo vs channels lookup, redirect paths). This is also the bulk of the SonarCloud duplication. codacy — duplicated `ownedChannel` ownership logic: valid, fixed. Now a single `src/lib/server/ownership.ts` used by the queue, rules, and audit-log servers (401 signed-out / 404 cross-owner, no existence leaks). codacy — duplicated cookie test helpers: valid, fixed. Moved to a shared test-only module `src/lib/server/testcookies.ts` (re-exported from `testdb.ts`), used by both OAuth test suites. Note: they could not live only in `testdb.ts` — that module registers a global `$lib/server/db` mock on import, which would override `oauth.test.ts`'s hand-rolled db mock. Incidental: `vite.config.ts` now excludes `.worktrees/**` from vitest so local parallel work in git worktrees doesn't pollute the suite. Snyk check: the failing `security/snyk` check reports no detail accessible from here and no dependency changed in this PR (`code/snyk` and CodeAnt SCA pass clean) — watching it on this push. |
….user_id index (migration 0006), secure state cookie, session purge
|
Review round 2 (qodo) — all five findings valid; fixed in 54f63e5 (162/162 tests, check 0 errors, build green): 1. Orphan claim race (action required): fixed, and it was worse than a race. Any new user created while orphans existed would claim them — so a signup ahead of the owner would steal the legacy channels. Find-or-create + claim is now a single transaction, and the claim only runs when the users table was empty before the insert — strictly the first-ever user, per the documented invariant. New test proves a second distinct signup does not claim a late orphan. (The transaction also exposed a libsql 2. Missing `channels.user_id` index: fixed. Schema index block + migration 0006 (`CREATE INDEX channels_user_id_idx`), applied to prod Turso (0004/0005 immutable as already deployed). 3. `oauth_state` cookie missing `secure`: fixed. `storePendingStates` now sets `secure` whenever `APP_URL` is https (stays off only for local http dev, matching the session cookie's protocol-aware behavior). 4. login/+page.svelte missing `<script lang="ts">`: fixed. 5. Expired sessions never purged: fixed. `createSession` now deletes `expires_at <= now` rows opportunistically — logins are infrequent, so this bounds buildup without a new scheduler; the cron path stays untouched. New test proves stale rows are purged. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/routes/api/auth/google/oauth.test.ts (1)
333-341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the two allowed reconnect cases.
The condition at Line 89 of
src/routes/api/auth/google/callback/+server.tshas three outcomes. Only the rejection path is tested. Add a test formocks.existingChannel = { userId: OWNER.id }, which must upsert and redirect, and a test formocks.existingChannel = { userId: null }, which must let the signed-in user claim the orphan channel. Without them, a future change that tightens the check toexisting && existing.userId !== user.idwould break orphan claiming and reconnects, and the suite would stay green.💚 Proposed tests
+test('callback lets the same owner reconnect an existing channel', async () => { + mocks.existingChannel = { userId: OWNER.id }; + stubTokenAndChannelResponses(); + + const thrown = await captureCallback(makeCookiesWithState('s'), { code: 'abc', state: 's' }); + + expect(thrown).toMatchObject({ status: 302 }); + expect(mocks.upserts).toHaveLength(1); +}); + +test('callback lets a signed-in user claim an ownerless channel', async () => { + mocks.existingChannel = { userId: null }; + stubTokenAndChannelResponses(); + + const thrown = await captureCallback(makeCookiesWithState('s'), { code: 'abc', state: 's' }); + + expect(thrown).toMatchObject({ status: 302 }); + expect((mocks.upserts[0].values as Record<string, unknown>).userId).toBe(OWNER.id); +});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/api/auth/google/oauth.test.ts` around lines 333 - 341, The current test only covers the rejection case of the three-way conditional at Line 89 in the callback server. Add two new test cases to the oauth.test.ts file to cover the allowed reconnect scenarios: one where mocks.existingChannel has userId equal to OWNER.id (the owner reconnecting their own channel, which should upsert and redirect), and another where mocks.existingChannel has userId as null (claiming an orphan channel, which should allow the signed-in user to take ownership). These additional tests prevent future regressions if the conditional logic is refactored.src/routes/api/auth/google/login/callback/+server.ts (1)
84-90: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not derive the
securecookie attribute fromurl.protocol.
secure: url.protocol === 'https:'depends on the protocol SvelteKit resolves for the request. Behind a TLS-terminating proxy that forwards plain HTTP without properORIGINconfiguration, this evaluates tofalse, and the session token is sent without theSecureattribute. A network attacker can then intercept the token over an unencrypted request.Derive the flag from deployment configuration. Import
devfrom$app/environmentand usesecure: !devto condition on build mode rather than request protocol.Note: The same vulnerability exists in
src/hooks.server.ts:35, which affects session cookie renewal on all requests.🔒 Proposed fix
+import { dev } from '$app/environment';cookies.set(SESSION_COOKIE, token, { path: '/', httpOnly: true, sameSite: 'lax', - secure: url.protocol === 'https:', + secure: !dev, expires: new Date(expiresAt) });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/api/auth/google/login/callback/`+server.ts around lines 84 - 90, Update the session cookie options in the callback handler to import `dev` from `$app/environment` and set `secure` to `!dev` instead of deriving it from `url.protocol`. Apply the same change to the session-cookie renewal logic in `handle` within `src/hooks.server.ts`, preserving the existing cookie behavior otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/server/google.ts`:
- Around line 70-104: In the token exchange flow, check tokenRes.ok immediately
after reading the response text and before JSON parsing. For non-OK responses,
use a small helper to safely extract non-secret error and error_description
fields for logging, fall back to a generic detail for invalid bodies, and throw
the existing userError; only parse and validate the success payload afterward,
removing the nested template literal and ternary around the current tokens error
logging.
In `@src/lib/server/testcookies.ts`:
- Around line 23-38: The delete method in makeCookies does not record deletion
calls like the set method does, which can cause logout tests to miss incorrect
cookie-clearing attributes. Add a deleteCalls array alongside setCalls to track
deletions. Update the delete method to accept an opts parameter matching the set
method's signature, then push an entry with the name and opts to deleteCalls
before removing from store. Return the deleteCalls array as a property of the
returned object so tests can verify deletion attributes.
In `@src/lib/server/testdb.ts`:
- Around line 68-69: Remove the makeCookies and makeCookiesWithState re-export
and its duplicate comment from testdb.ts. Update login.test.ts to import those
helpers from $lib/server/testcookies, matching oauth.test.ts, so testcookies.ts
is the sole import path.
- Line 84: The createTestDb function does not enable SQLite foreign-key
enforcement, so the REFERENCES and ON DELETE CASCADE constraints in the schema
are not actually enforced during tests. Add an execution of the PRAGMA
foreign_keys = ON statement in the createTestDb function before the schema
creation steps so that foreign-key constraints are properly enforced during test
execution.
---
Outside diff comments:
In `@src/routes/api/auth/google/login/callback/`+server.ts:
- Around line 84-90: Update the session cookie options in the callback handler
to import `dev` from `$app/environment` and set `secure` to `!dev` instead of
deriving it from `url.protocol`. Apply the same change to the session-cookie
renewal logic in `handle` within `src/hooks.server.ts`, preserving the existing
cookie behavior otherwise.
In `@src/routes/api/auth/google/oauth.test.ts`:
- Around line 333-341: The current test only covers the rejection case of the
three-way conditional at Line 89 in the callback server. Add two new test cases
to the oauth.test.ts file to cover the allowed reconnect scenarios: one where
mocks.existingChannel has userId equal to OWNER.id (the owner reconnecting their
own channel, which should upsert and redirect), and another where
mocks.existingChannel has userId as null (claiming an orphan channel, which
should allow the signed-in user to take ownership). These additional tests
prevent future regressions if the conditional logic is refactored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5a03a687-84ad-490d-8cac-5108f9590f95
📒 Files selected for processing (16)
drizzle/0005_common_texas_twister.sqldrizzle/meta/0005_snapshot.jsondrizzle/meta/_journal.jsonsrc/lib/server/db/schema.tssrc/lib/server/google.tssrc/lib/server/ownership.tssrc/lib/server/testcookies.tssrc/lib/server/testdb.tssrc/routes/(app)/channels/[id]/log/+page.server.tssrc/routes/(app)/channels/[id]/queue/+page.server.tssrc/routes/(app)/channels/[id]/rules/+page.server.tssrc/routes/api/auth/google/callback/+server.tssrc/routes/api/auth/google/login/callback/+server.tssrc/routes/api/auth/google/login/login.test.tssrc/routes/api/auth/google/oauth.test.tsvite.config.ts
…nnel upsert, resilient hooks, guarded logout
|
Round 3 (CodeRabbit full review) addressed in Fixed this round:
Skipped, with reasons:
Stale (already fixed in earlier rounds, no action needed): 0004 FK, session sweep, queue ownership extraction, login/callback exchange extraction, login-test cookie helpers. |
|
Codacy annotations on
Note: the CodeRabbit reviews stamped 13:57/14:08 UTC ran against |
|
qodo suggestions (up to Stale — already fixed, verified against current code:
Still valid — fixed in
Tests: queue/rules/log loads each assert the returned |
Not up to standards ⛔🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | |
| Duplication |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
Thanks for the thorough triage. The listed fixes and validation results look good. The only remaining indexed item is finding 5 ( |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/routes/(app)/channels/[id]/queue/+page.server.ts (1)
88-109: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winAuthenticate before validating
commentId.Each handler returns
fail(400, ...)beforeactreachesownedChannel. A signed-out request with malformed form data therefore bypasses the required authentication path. CallrequireUser(locals)before parsing the form, or move parsing after the existing ownership check in a shared handler. Add a signed-out malformed-form test.As per coding guidelines, every form action must call
requireUser(locals).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/`(app)/channels/[id]/queue/+page.server.ts around lines 88 - 109, Ensure every form action in the actions object, including approve, reject, del, and ban, calls requireUser(locals) before parsing or validating commentId. Preserve the existing act calls and responses, and add coverage for a signed-out request with malformed form data.Source: Coding guidelines
src/routes/api/auth/google/callback/+server.ts (1)
85-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEncrypt the refresh token once.
encrypt(tokens.refreshToken)runs on Line 91 and again on Line 97. Only one result is ever stored. Hoist the value into a constant.The conditional upsert itself is correct: SQLite skips the
DO UPDATEwhensetWhereis false, so.returning()yields no row and the handler answers 409.♻️ Proposed refactor
+ const refreshTokenEnc = encrypt(tokens.refreshToken); const updated = await db .insert(channels) .values({ id: ch.id, userId: user.id, title, - refreshTokenEnc: encrypt(tokens.refreshToken), + refreshTokenEnc, active: 1, createdAt: new Date().toISOString() }) .onConflictDoUpdate({ target: channels.id, - set: { userId: user.id, title, refreshTokenEnc: encrypt(tokens.refreshToken), active: 1 }, + set: { userId: user.id, title, refreshTokenEnc, active: 1 }, setWhere: or(isNull(channels.userId), eq(channels.userId, user.id)) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/api/auth/google/callback/`+server.ts around lines 85 - 103, In the channel upsert flow, compute the encrypted refresh token once in a local constant before the db.insert call, then reuse that constant for both the values.refreshTokenEnc field and the onConflictDoUpdate set.refreshTokenEnc field.src/routes/api/auth/google/oauth.test.ts (1)
333-344: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winCover the ownership predicate against a real database.
This test proves that the route maps an empty
returning()result to 409, and that somesetWherepredicate was passed. It cannot prove that the predicate blocks a foreign owner, because the mock decides the outcome frommocks.existingChannel. An incorrect predicate, for exampleeq(channels.userId, user.id)without theisNullbranch, still passes here.Add one test that uses
setupTestDbwith realchannelsrows, assrc/routes/api/auth/google/login/login.test.tsdoes. Assert three cases: a new channel inserts, a channel owned by the caller updates, and a channel owned by another user stays unchanged and yields 409.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/api/auth/google/oauth.test.ts` around lines 333 - 344, Add a new integration test following the pattern in src/routes/api/auth/google/login/login.test.ts that uses setupTestDb with real channels table rows instead of mocks to verify the ownership predicate in the callback function actually works correctly. The test should assert three scenarios: inserting a new channel when none exists, updating an existing channel owned by the current user, and rejecting with 409 status when a channel is already owned by a different user while keeping that row unchanged in the database.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/hooks.server.ts`:
- Around line 28-44: Update the session lookup catch block in
src/hooks.server.ts lines 28-44 to keep the server-side logging but return the
established generic user-visible server error instead of assigning
event.locals.user = null. Update the session lookup test in
src/hooks.server.test.ts lines 40-50 to replace the successful-resolution
assertion with verification of that generic failure response.
In `@src/lib/server/google.ts`:
- Around line 75-91: In the non-OK response handling of the token request,
update the nested error-description construction inside the detail assignment to
first build the description in a separate statement, then combine it with
body.error. Preserve the existing fallback, type checks, sanitized logging, and
error response behavior in the surrounding token status check.
In `@src/lib/server/oauthState.ts`:
- Around line 58-60: Create a single validated helper function that derives the
cookie Secure attribute from env.APP_URL and throws or logs when APP_URL is
missing, replacing the two separate unvalidated computations. In
src/lib/server/oauthState.ts line 58-60, replace the (env.APP_URL ??
'').startsWith('https://') expression with a call to this helper. In
src/routes/api/auth/google/login/callback/+server.ts line 102-109, replace the
secure: url.protocol === 'https:' expression with the same helper, ensuring both
cookies derive their Secure attribute from the same validated source and
preventing silent fallbacks when APP_URL is missing.
In `@src/lib/server/session.test.ts`:
- Around line 81-91: Update the test creating a session purges already-expired
rows to also seed a non-expired session, then assert that its ID remains after
createSession(userId) while stale-token is removed. Keep the existing
expired-row assertion and use distinct session identifiers for both rows.
In `@src/routes/`(app)/channels/[id]/log/load.test.ts:
- Around line 29-38: Add authorization-regression tests for both protected
paths: in src/routes/(app)/channels/[id]/log/load.test.ts lines 29-38, add
foreign-owner 404 and signed-out 401 cases for load; in
src/routes/(app)/channels/[id]/rules/actions.test.ts lines 87-101, invoke
actions.add as a foreign and signed-out user and assert no rule is inserted. Use
the existing test setup and ensure the assertions fail if ownership or
authentication checks are removed.
In `@src/routes/api/auth/google/login/login.test.ts`:
- Around line 180-199: Remove the redundant test that begins at line 213-223
(which also tests that the first login claims the orphaned channel UC1). The
combined test "only the first-ever user claims orphaned channels" at lines
180-199 already covers this original assertion in its first section and adds the
new distinct assertion that a subsequent signup does not claim a different
orphaned channel. Keep the combined test and delete the now-redundant earlier
test to eliminate the duplication.
- Around line 146-149: Update the log assertion in the test around errorSpy to
use a dedicated matcher that directly verifies the spy’s call count is greater
than zero, so failures report the actual count instead of asserting on a derived
boolean. Leave the oauth_state cookie assertion unchanged.
In `@src/routes/login/`+page.svelte:
- Around line 25-27: The empty script block in the Svelte component contains
only a comment about the static sign-in prompt and guard redirect. Move this
comment from inside the script block into the markup as an HTML comment placed
above the main element, then delete the now-empty script block entirely.
---
Outside diff comments:
In `@src/routes/`(app)/channels/[id]/queue/+page.server.ts:
- Around line 88-109: Ensure every form action in the actions object, including
approve, reject, del, and ban, calls requireUser(locals) before parsing or
validating commentId. Preserve the existing act calls and responses, and add
coverage for a signed-out request with malformed form data.
In `@src/routes/api/auth/google/callback/`+server.ts:
- Around line 85-103: In the channel upsert flow, compute the encrypted refresh
token once in a local constant before the db.insert call, then reuse that
constant for both the values.refreshTokenEnc field and the onConflictDoUpdate
set.refreshTokenEnc field.
In `@src/routes/api/auth/google/oauth.test.ts`:
- Around line 333-344: Add a new integration test following the pattern in
src/routes/api/auth/google/login/login.test.ts that uses setupTestDb with real
channels table rows instead of mocks to verify the ownership predicate in the
callback function actually works correctly. The test should assert three
scenarios: inserting a new channel when none exists, updating an existing
channel owned by the current user, and rejecting with 409 status when a channel
is already owned by a different user while keeping that row unchanged in the
database.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b0d88762-1f47-46fe-9f88-8e6c0a282294
📒 Files selected for processing (26)
EXECUTION_PLAN_YouTube_Comment_Moderator.mddrizzle/0006_huge_boom_boom.sqldrizzle/meta/0006_snapshot.jsondrizzle/meta/_journal.jsonsrc/hooks.server.test.tssrc/hooks.server.tssrc/lib/server/db/schema.tssrc/lib/server/google.tssrc/lib/server/oauthState.tssrc/lib/server/session.test.tssrc/lib/server/session.tssrc/lib/server/testcookies.tssrc/lib/server/testdb.tssrc/routes/(app)/channels/[id]/log/+page.server.tssrc/routes/(app)/channels/[id]/log/load.test.tssrc/routes/(app)/channels/[id]/queue/+page.server.tssrc/routes/(app)/channels/[id]/queue/actions.test.tssrc/routes/(app)/channels/[id]/rules/+page.server.tssrc/routes/(app)/channels/[id]/rules/actions.test.tssrc/routes/api/auth/google/callback/+server.tssrc/routes/api/auth/google/login/callback/+server.tssrc/routes/api/auth/google/login/login.test.tssrc/routes/api/auth/google/oauth.test.tssrc/routes/login/+page.sveltesrc/routes/logout/+page.server.tssrc/routes/logout/logout.test.ts
User descriptionWhatMulti-user accounts so anyone can sign up, connect their YouTube channel, and use the tool — per the approved plan.
Verify
Deploy notesAfter merge: run migration 0004 on prod (additive, no data rewrite). First Google sign-in on the existing DB claims the orphaned channels — sign in with the owner account first. Add CodeAnt-AI DescriptionAdd Google accounts with secure, user-owned YouTube moderation What Changed
Impact
💡 Usage GuideChecking Your Pull RequestEvery time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later. Talking to CodeAnt AIGot a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask: This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code. ExamplePreserve Org Learnings with CodeAntYou can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input: This helps CodeAnt AI learn and adapt to your team's coding style and standards. ExampleRetrigger reviewAsk CodeAnt AI to review the PR again, by typing: Check Your Repository HealthTo analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health. |
PR Code Suggestions ✨Latest suggestions up to commit
|
| Category | Suggestion | Severity |
| Security |
Concurrent OAuth callbacks can restore an already-consumed state and break the one-time state guaranteeThe OAuth state cookie is consumed using a stale read-modify-write sequence. If two src/routes/api/auth/google/callback/+server.ts [84] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/api/auth/google/callback/+server.ts
**Line:** 84:84
**Comment:**
*Security: The OAuth state cookie is consumed using a stale read-modify-write sequence. If two callbacks run concurrently, both can read the same `pending` array; one response can remove a state while the other writes its stale filtered array and resurrects the already-consumed state. Because the state is not stored server-side or atomically removed, a successful OAuth transaction is not reliably one-time. Consume states with a server-side one-time record or otherwise serialize/validate the cookie update.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major |
| Race condition |
Concurrent requests can perform multiple external moderation actions for the same pending commentThe ownership check and pending-comment lookup are separate reads, and the comment src/routes/(app)/channels/[id]/queue/+page.server.ts [44] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/(app)/channels/[id]/queue/+page.server.ts
**Line:** 44:44
**Comment:**
*Race Condition: The ownership check and pending-comment lookup are separate reads, and the comment remains pending while the external YouTube request runs. Concurrent submissions or retries can therefore both pass `ownedChannel` and the pending check, issue multiple moderation actions, and then overwrite the local status and audit records. Atomically claim the pending comment before calling YouTube, or otherwise serialize actions per comment.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major |
# Conflicts: # src/routes/+page.svelte
|
Merged updated What came in: PR #26 (night-shift-ink landing redesign — new Conflict: exactly one — Verification on the merged tree: 195/195 tests (including the new landing/tone suites), svelte-check 0 errors, production build green. No breakage in the |
|
Warning Review limit reached
Next review available in: 27 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
Comment |
|
Two latest review findings triaged against Valid — fixed: queue Technically correct but skipped: OAuth state cookie read-modify-write. The observation is right — two concurrent successful callbacks from the same browser can resurrect an already-consumed state. But per the execution plan (§5b), the state cookie is deliberately a CSRF binding ("bind the auth request to this browser session"), not a one-time replay token: the one-time artifact in the flow is Google's authorization |
|




What
Multi-user accounts so anyone can sign up, connect their YouTube channel, and use the tool — per the approved plan.
/api/auth/google/login, scopesopenid email profile). Two-step by design: identity first, then the existingyoutube.force-sslconsent at/api/auth/google(URLs unchanged, so no Google console changes).src/lib/server/session.ts— random 32-byte token,sessionstable, httpOnlymoderaty_sessioncookie, 30-day sliding expiry.hooks.server.tspopulateslocals.user.(app)layout redirects signed-out visitors to/login; every form action callsrequireUser(locals)(401 backstop). New/loginpage,/logoutaction, account chip + sign-out in the app header; landing-page CTAs now go to/login.channels.userId— dashboard (channels, stats, bans),setToneLevel, rules add/remove, review-queue actions, audit log. Cross-owner always 404 (no existence leaks). Connect callback attachesuserIdand refuses (409) reattaching a channel owned by another account. This also resolves the earlier CodeRabbitsetToneLevelauth finding properly.users(google_subunique,plandefault'free'— future Stripe hook),sessions,channels.user_id. Orphaned pre-accounts channels are claimed by the first user ever to sign in — that's how the existing prod DB attaches to its owner.Verify
npm run test— 160/160 (new: session lib, login flow incl. find-or-create/orphan-claim/cross-owner, scoping + 401 tests on every action)npm run check— 0 errors ·npm run build— greenEXECUTION_PLAN(§5b accounts phase + §7 Stripe future),AGENTS.md(Accounts & Sessions),README.md(accounts/BYOK)Deploy notes
After merge: run migration 0004 on prod (additive, no data rewrite). First Google sign-in on the existing DB claims the orphaned channels — sign in with the owner account first. Add
http://localhost:5173/api/auth/google/login/callbackand the prod equivalent to the Google OAuth client's authorized redirect URIs.