Skip to content
Merged
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
96 changes: 88 additions & 8 deletions frontend/src/components/items/ApplicationSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Check, Moon, Sun } from 'lucide-react';
import { Check, Clock, Moon, Sun } from 'lucide-react';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';

import { useTheme } from '@/theme/ThemeProvider';

import { settings } from '../../../wailsjs/go/models';
import { GetSettings, UpdateSettings } from '../../../wailsjs/go/settings/Service';

function ThemeSwatch({
active,
dark,
Expand Down Expand Up @@ -51,19 +56,94 @@ function ThemeSwatch({
);
}

const timeoutOptions = [
{ value: 1, label: '1 minute' },
{ value: 5, label: '5 minutes' },
{ value: 15, label: '15 minutes' },
{ value: 30, label: '30 minutes' },
{ value: 0, label: 'Disabled' },
];

export default function ApplicationSettings() {
const { theme, setTheme } = useTheme();
const isDark = theme === 'dark';

const [currentSettings, setCurrentSettings] = useState<settings.Settings | null>(null);
const [inactivityTimeout, setInactivityTimeout] = useState<number>(15);

useEffect(() => {
GetSettings()
.then((s: settings.Settings) => {
if (s) {
setCurrentSettings(s);
const timeout = (s as unknown as { InactivityTimeoutMinutes?: number })
.InactivityTimeoutMinutes;
setInactivityTimeout(timeout ?? 15);
}
})
.catch(() => {});
}, []);

const handleTimeoutChange = async (minutes: number) => {
try {
const input = new settings.UpdateSettingsInput({
StorageMode: currentSettings?.StorageMode || 'local',
CloudKeys: currentSettings?.CloudKeys || [],
ErasureCoding: currentSettings?.ErasureCoding || false,
ErasureCodingConfig: currentSettings?.ErasureCodingConfig || '2+2',
InactivityTimeoutMinutes: minutes,
});

await UpdateSettings(input);
setInactivityTimeout(minutes);
toast.success('Inactivity timeout setting saved.');
} catch {
toast.error('Failed to update inactivity timeout.');
}
};

return (
<div className="rounded-2xl border border-border bg-surface backdrop-blur-sm dark:border-border-strong">
<div className="p-6">
<h3 className="text-base font-bold text-text">Appearance</h3>
<p className="mt-1 text-sm text-text-muted">Choose how ayo looks on your device.</p>
<div className="space-y-4">
<div className="rounded-2xl border border-border bg-surface backdrop-blur-sm dark:border-border-strong">
<div className="p-6">
<h3 className="text-base font-bold text-text">Appearance</h3>
<p className="mt-1 text-sm text-text-muted">Choose how ayo looks on your device.</p>

<div className="mt-5 flex items-center gap-3">
<ThemeSwatch active={!isDark} dark={false} onClick={() => setTheme('light')} />
<ThemeSwatch active={isDark} dark onClick={() => setTheme('dark')} />
<div className="mt-5 flex items-center gap-3">
<ThemeSwatch active={!isDark} dark={false} onClick={() => setTheme('light')} />
<ThemeSwatch active={isDark} dark onClick={() => setTheme('dark')} />
</div>
</div>
</div>

<div className="rounded-2xl border border-border bg-surface backdrop-blur-sm dark:border-border-strong">
<div className="p-6">
<div className="flex items-center gap-2">
<Clock className="h-5 w-5 text-primary" />
<h3 className="text-base font-bold text-text">Session Security & Auto-Lock</h3>
</div>
<p className="mt-1 text-sm text-text-muted">
Automatically lock your signed-in session and close the database connection after a
period of inactivity.
</p>

<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-5">
{timeoutOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => handleTimeoutChange(opt.value)}
className={`flex items-center justify-between rounded-xl border p-3 text-xs font-semibold transition-all duration-200 ${
inactivityTimeout === opt.value
? 'border-primary bg-primary/10 text-primary ring-2 ring-primary/20'
: 'border-border text-text-muted hover:border-primary/50 dark:border-border-strong'
}`}
>
<span>{opt.label}</span>
{inactivityTimeout === opt.value && <Check className="h-3.5 w-3.5 text-primary" />}
</button>
))}
</div>
</div>
</div>
</div>
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Register as RegisterService,
ResetPassword as ResetPasswordService,
SaveRecoveryKey as SaveRecoveryKeyService,
TouchSession,
} from '../../wailsjs/go/auth/Service';
import { auth } from '../../wailsjs/go/models';

Expand Down Expand Up @@ -77,6 +78,52 @@ export function AuthProvider({ children }: { children: ReactNode }) {
refreshSession();
}, []);

useEffect(() => {
if (!session) return;

let hasActivity = false;

const handleUserActivity = () => {
hasActivity = true;
};

window.addEventListener('mousemove', handleUserActivity, { passive: true });
window.addEventListener('keydown', handleUserActivity, { passive: true });
window.addEventListener('click', handleUserActivity, { passive: true });
window.addEventListener('scroll', handleUserActivity, { passive: true });

// Touch session every 10s if user performed interaction
const activityInterval = setInterval(() => {
if (hasActivity) {
hasActivity = false;
if (typeof TouchSession === 'function') {
TouchSession().catch(() => {});
}
}
}, 10000);

// Check for expiration every 5s
const checkInterval = setInterval(async () => {
try {
const currentSession = await GetSession();
if (!currentSession || currentSession.UserId === 0) {
setSession(null);
}
} catch {
setSession(null);
}
}, 5000);

return () => {
window.removeEventListener('mousemove', handleUserActivity);
window.removeEventListener('keydown', handleUserActivity);
window.removeEventListener('click', handleUserActivity);
window.removeEventListener('scroll', handleUserActivity);
clearInterval(activityInterval);
clearInterval(checkInterval);
};
}, [session]);

const login = async (input: auth.LoginInput) => {
const success = await LoginService(input);
if (success) {
Expand Down
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/auth/Service.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export function ResetPassword(arg1:auth.ResetPasswordInput):Promise<auth.Registe

export function SaveRecoveryKey(arg1:string,arg2:string):Promise<void>;

export function SetInactivityTimeout(arg1:number):Promise<void>;

export function SetMasterKeyStorage(arg1:string):Promise<string>;

export function Startup(arg1:context.Context):Promise<void>;

export function TouchSession():Promise<void>;
8 changes: 8 additions & 0 deletions frontend/wailsjs/go/auth/Service.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,18 @@ export function SaveRecoveryKey(arg1, arg2) {
return window['go']['auth']['Service']['SaveRecoveryKey'](arg1, arg2);
}

export function SetInactivityTimeout(arg1) {
return window['go']['auth']['Service']['SetInactivityTimeout'](arg1);
}

export function SetMasterKeyStorage(arg1) {
return window['go']['auth']['Service']['SetMasterKeyStorage'](arg1);
}

export function Startup(arg1) {
return window['go']['auth']['Service']['Startup'](arg1);
}

export function TouchSession() {
return window['go']['auth']['Service']['TouchSession']();
}
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ export namespace settings {
CloudKeys: any[];
ErasureCoding: boolean;
ErasureCodingConfig: string;
InactivityTimeoutMinutes: number;

static createFrom(source: any = {}) {
return new Settings(source);
Expand All @@ -399,13 +400,15 @@ export namespace settings {
this.CloudKeys = source["CloudKeys"];
this.ErasureCoding = source["ErasureCoding"];
this.ErasureCodingConfig = source["ErasureCodingConfig"];
this.InactivityTimeoutMinutes = source["InactivityTimeoutMinutes"];
}
}
export class UpdateSettingsInput {
StorageMode: string;
CloudKeys: any[];
ErasureCoding: boolean;
ErasureCodingConfig: string;
InactivityTimeoutMinutes: number;

static createFrom(source: any = {}) {
return new UpdateSettingsInput(source);
Expand All @@ -417,6 +420,7 @@ export namespace settings {
this.CloudKeys = source["CloudKeys"];
this.ErasureCoding = source["ErasureCoding"];
this.ErasureCodingConfig = source["ErasureCodingConfig"];
this.InactivityTimeoutMinutes = source["InactivityTimeoutMinutes"];
}
}

Expand Down
Loading
Loading