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
81 changes: 67 additions & 14 deletions apps/mail/app/(routes)/settings/security/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { m } from '@/paraglide/messages';
import { useForm } from 'react-hook-form';

import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';
import { toast } from 'sonner';
import { useEffect } from 'react';
import * as z from 'zod';

const formSchema = z.object({
Expand All @@ -22,7 +24,19 @@ const formSchema = z.object({
});

export default function SecurityPage() {
const [isSaving, setIsSaving] = useState(false);
const trpc = useTRPC();
const queryClient = useQueryClient();

// Fetch current user settings
const { data: settingsData, isLoading } = useQuery({
...trpc.settings.get.queryOptions(),
select: (data: any) => data?.settings,
});

// Save settings mutation
const { mutateAsync: saveUserSettings, isPending: isSaving } = useMutation(
trpc.settings.save.mutationOptions(),
);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
Expand All @@ -32,14 +46,45 @@ export default function SecurityPage() {
},
});

function onSubmit(values: z.infer<typeof formSchema>) {
setIsSaving(true);
// Update form when settings are loaded
useEffect(() => {
if (settingsData) {
form.reset({
twoFactorAuth: settingsData.twoFactorAuth ?? false,
loginNotifications: settingsData.loginNotifications ?? true,
});
}
}, [settingsData, form]);

async function onSubmit(values: z.infer<typeof formSchema>) {
const saved = settingsData? { ...settingsData } : undefined;

try {
// Optimistically update the UI
queryClient.setQueryData(trpc.settings.get.queryKey(), (updater: any) => {
if (!updater) return;
return { settings: { ...updater.settings, ...values } };
});

// TODO: Save settings in user's account
setTimeout(() => {
console.log(values);
setIsSaving(false);
}, 1000);
await saveUserSettings({
twoFactorAuth: values.twoFactorAuth,
loginNotifications: values.loginNotifications,
});

toast.success(m['common.settings.saved']());
} catch (error) {
console.error('Failed to save security settings:', error);
toast.error(m['common.settings.failedToSave']());

// Revert optimistic update on error
queryClient.setQueryData(trpc.settings.get.queryKey(), (prev: any) => {
if (!prev || !saved) return;
return {
...prev,
settings: { ...(prev?.settings ?? {}), ...saved },
};
});
}
}

return (
Expand All @@ -50,7 +95,7 @@ export default function SecurityPage() {
footer={
<div className="flex gap-4">
<Button variant="destructive">{m['pages.settings.security.deleteAccount']()}</Button>
<Button type="submit" form="security-form" disabled={isSaving}>
<Button type="submit" form="security-form" disabled={isSaving || isLoading}>
{isSaving ? m['common.actions.saving']() : m['common.actions.saveChanges']()}
</Button>
</div>
Expand All @@ -73,7 +118,11 @@ export default function SecurityPage() {
</FormDescription>
</div>
<FormControl className="ml-4">
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isLoading}
/>
</FormControl>
</FormItem>
)}
Expand All @@ -92,7 +141,11 @@ export default function SecurityPage() {
</FormDescription>
</div>
<FormControl className="ml-4">
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isLoading}
/>
</FormControl>
</FormItem>
)}
Expand All @@ -103,4 +156,4 @@ export default function SecurityPage() {
</SettingsCard>
</div>
);
}
}
47 changes: 47 additions & 0 deletions apps/server/src/db/migrations/0038_clean_franklin_storm.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
ALTER TABLE "mail0_account" DROP CONSTRAINT "mail0_account_user_id_mail0_user_id_fk";
--> statement-breakpoint
ALTER TABLE "mail0_connection" DROP CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk";
--> statement-breakpoint
ALTER TABLE "mail0_session" DROP CONSTRAINT "mail0_session_user_id_mail0_user_id_fk";
--> statement-breakpoint
ALTER TABLE "mail0_user_hotkeys" DROP CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk";
--> statement-breakpoint
ALTER TABLE "mail0_user_settings" DROP CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk";
--> statement-breakpoint
ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"autoRead":true,"defaultEmailAlias":"","categories":[{"id":"Important","name":"Important","searchValue":"IMPORTANT","order":0,"icon":"Lightning","isDefault":false},{"id":"All Mail","name":"All Mail","searchValue":"","order":1,"icon":"Mail","isDefault":true},{"id":"Unread","name":"Unread","searchValue":"UNREAD","order":5,"icon":"ScanEye","isDefault":false}],"undoSendEnabled":false,"imageCompression":"medium","animations":false,"twoFactorAuth":false,"loginNotifications":true}'::jsonb;--> statement-breakpoint
ALTER TABLE "mail0_account" ADD CONSTRAINT "mail0_account_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mail0_connection" ADD CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mail0_session" ADD CONSTRAINT "mail0_session_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mail0_summary" ADD CONSTRAINT "mail0_summary_connection_id_mail0_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."mail0_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mail0_user_hotkeys" ADD CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mail0_user_settings" ADD CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_user_id_idx" ON "mail0_account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "account_provider_user_id_idx" ON "mail0_account" USING btree ("provider_id","user_id");--> statement-breakpoint
CREATE INDEX "account_expires_at_idx" ON "mail0_account" USING btree ("access_token_expires_at");--> statement-breakpoint
CREATE INDEX "connection_user_id_idx" ON "mail0_connection" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "connection_expires_at_idx" ON "mail0_connection" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "connection_provider_id_idx" ON "mail0_connection" USING btree ("provider_id");--> statement-breakpoint
CREATE INDEX "early_access_is_early_access_idx" ON "mail0_early_access" USING btree ("is_early_access");--> statement-breakpoint
CREATE INDEX "jwks_created_at_idx" ON "mail0_jwks" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "note_user_id_idx" ON "mail0_note" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "note_thread_id_idx" ON "mail0_note" USING btree ("thread_id");--> statement-breakpoint
CREATE INDEX "note_user_thread_idx" ON "mail0_note" USING btree ("user_id","thread_id");--> statement-breakpoint
CREATE INDEX "note_is_pinned_idx" ON "mail0_note" USING btree ("is_pinned");--> statement-breakpoint
CREATE INDEX "oauth_access_token_user_id_idx" ON "mail0_oauth_access_token" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "oauth_access_token_client_id_idx" ON "mail0_oauth_access_token" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX "oauth_access_token_expires_at_idx" ON "mail0_oauth_access_token" USING btree ("access_token_expires_at");--> statement-breakpoint
CREATE INDEX "oauth_application_user_id_idx" ON "mail0_oauth_application" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "oauth_application_disabled_idx" ON "mail0_oauth_application" USING btree ("disabled");--> statement-breakpoint
CREATE INDEX "oauth_consent_user_id_idx" ON "mail0_oauth_consent" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "oauth_consent_client_id_idx" ON "mail0_oauth_consent" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX "oauth_consent_given_idx" ON "mail0_oauth_consent" USING btree ("consent_given");--> statement-breakpoint
CREATE INDEX "session_user_id_idx" ON "mail0_session" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "session_expires_at_idx" ON "mail0_session" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "summary_connection_id_idx" ON "mail0_summary" USING btree ("connection_id");--> statement-breakpoint
CREATE INDEX "summary_connection_id_saved_idx" ON "mail0_summary" USING btree ("connection_id","saved");--> statement-breakpoint
CREATE INDEX "summary_saved_idx" ON "mail0_summary" USING btree ("saved");--> statement-breakpoint
CREATE INDEX "user_hotkeys_shortcuts_idx" ON "mail0_user_hotkeys" USING btree ("shortcuts");--> statement-breakpoint
CREATE INDEX "user_settings_settings_idx" ON "mail0_user_settings" USING btree ("settings");--> statement-breakpoint
CREATE INDEX "verification_identifier_idx" ON "mail0_verification" USING btree ("identifier");--> statement-breakpoint
CREATE INDEX "verification_expires_at_idx" ON "mail0_verification" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "writing_style_matrix_style_idx" ON "mail0_writing_style_matrix" USING btree ("style");
Loading
Loading