Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion src/app/actions/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { getServerSupabase } from '@/lib/supabase/server';
import { getServiceSupabase } from '@/lib/supabase/service';
import { inngest } from '@/inngest/client';
import { rateLimit } from '@/lib/rate-limit';
import { ok, err, type Result } from '@/lib/result';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { rateLimit } from '@/lib/rate-limit';

type BootstrapOutput = {
profileId: string;
Expand Down Expand Up @@ -38,6 +38,7 @@ export async function bootstrapProfile(): Promise<Result<BootstrapOutput>> {
const githubId = String(identity.id);
const githubHandle = (identity.identity_data?.['user_name'] ??
identity.identity_data?.['preferred_username']) as string | undefined;

if (!githubHandle) return err('no_github_handle', 'GitHub handle missing from identity');

const avatarUrl = identity.identity_data?.['avatar_url'] as string | undefined;
Expand Down Expand Up @@ -68,8 +69,10 @@ export async function bootstrapProfile(): Promise<Result<BootstrapOutput>> {
}

let auditQueued = false;

if (!profile.audit_completed) {
const providerToken = (await sb.auth.getSession()).data.session?.provider_token;

if (providerToken) {
await inngest.send({
name: 'audit/run',
Expand All @@ -80,6 +83,7 @@ export async function bootstrapProfile(): Promise<Result<BootstrapOutput>> {
accessToken: providerToken,
},
});

auditQueued = true;
}
}
Expand Down Expand Up @@ -108,15 +112,72 @@ export async function bootstrapProfile(): Promise<Result<BootstrapOutput>> {
});
}

/**
* Updates or clears the user's mute preferences.
* Pass empty arrays to clear preferences.
*/
export async function updateMutePreferences(
mutedRepos: string[],
mutedLanguages: string[],
): Promise<Result<void>> {
const sb = getServerSupabase();

if (!sb) {
return err('not_configured', 'auth not configured');
}

const {
data: { user },
} = await sb.auth.getUser();

if (!user) {
return err('not_authenticated', 'sign in first');
}

const rateRes = await rateLimit({
namespace: 'profile:mute',
key: user.id,
limit: 10,
windowSec: 60,
});

if (!rateRes.ok) {
return err('rate_limited', 'slow down', true);
}

const service = getServiceSupabase();

if (!service) {
return err('not_configured', 'service role not configured');
}

const { error: updateErr } = await service
.from('profiles')
.update({
muted_repos: mutedRepos,
muted_languages: mutedLanguages,
})
.eq('id', user.id);

if (updateErr) {
return err('persist_failed', updateErr.message);
}

return ok(undefined);
}

// ============================================================================
// Profile Update Action
// ============================================================================

// Validation schema for profile updates
const profileUpdateSchema = z.object({
bio: z.string().max(280, 'Bio must be 280 characters or less').optional().nullable(),

skills: z.array(z.string()).max(10, 'Maximum 10 skills allowed').optional().nullable(),

website_url: z.string().url('Please enter a valid URL').optional().nullable().or(z.literal('')),

twitter_handle: z
.string()
.regex(/^[A-Za-z0-9_]{1,15}$/, 'Invalid Twitter handle (no @ symbol, max 15 chars)')
Expand All @@ -132,6 +193,7 @@ export type ProfileUpdateData = z.infer<typeof profileUpdateSchema>;
*/
export async function updateProfile(data: ProfileUpdateData): Promise<Result<{ message: string }>> {
const sb = getServerSupabase();

if (!sb) {
return err('not_configured', 'Authentication not configured');
}
Expand Down
8 changes: 7 additions & 1 deletion src/app/actions/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export async function linkPrToRec(recId: number, prUrl: string): Promise<Result<

export async function skipRecommendation(
recId: number,
skipReason?: string,
): Promise<Result<{ id: number; replacement: RecCard | null }>> {
const sb = getServerSupabase();
if (!sb) return err('not_configured', 'auth not configured');
Expand All @@ -214,9 +215,14 @@ export async function skipRecommendation(
if (!rateRes.ok) return err('rate_limited', 'slow down', true);

// Atomic skip with the issue id so we know what tier to refill from.
// Persist the optional skip_reason alongside the status change.
const updatePayload: Record<string, unknown> = { status: 'reassigned' };
if (skipReason?.trim()) {
updatePayload.skip_reason = skipReason.trim().slice(0, 500);
}
const { data, error: updateErr } = await service
.from('recommendations')
.update({ status: 'reassigned' })
.update(updatePayload)
.eq('id', recId)
.eq('user_id', user.id)
.eq('status', 'open')
Expand Down
Loading
Loading