Skip to content

Commit 3467aac

Browse files
adding the option to persist the login for 30 days. (#25)
1 parent 933b8d4 commit 3467aac

File tree

9 files changed

+311
-156
lines changed

9 files changed

+311
-156
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Privacy focused, cross-platform clipboard manager",
55
"author": "Daniel Railean <me@ddlele.com>",
66
"license": "No licence, rights reserved",
7-
"version": "0.8.6-2april2025",
7+
"version": "0.8.6-8june2025",
88
"main": "dist/index.js",
99
"repository": {
1010
"type": "git",

src/electron/App/EventHandler.ts

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import { DateTime } from 'luxon'
55
import { ItemRepo } from '../Data/ItemRepository'
66
import { RequestService, userTokens } from '../Data/Requests'
77
import { IClipboardItem, isImageContent, isRTFContent, isTextContent, RemoteItemStatus } from '../DataModels/DataTypes'
8-
import { IAppState, IKeyboardEvent, IMouseEvent, IReceiveChannel } from '../DataModels/LocalTypes'
8+
import { IAppState, IKeyboardEvent, IMouseEvent, IReceiveChannel, ISavedUser } from '../DataModels/LocalTypes'
99
import { CryptoService } from '../Utils/CryptoService'
1010
import { JsUtil } from '../Utils/JsUtil'
1111
import { AppSettings } from './AppSettings'
1212
import { messages } from './CommunicationMessages'
13-
import { InitUserSettings, IUserPreferences, store, userPreferences } from './UserPreferences'
13+
import { clearUserData, getUserData, InitUserSettings, IUserPreferences, store, storeUserData, userPreferences } from './UserPreferences'
1414

1515
const shortcutsKey = `fileclip.shortcuts`
1616

1717
export const state: IAppState = {
18-
pullInterval: undefined,
18+
clipboardPullInterval: undefined,
1919
remoteSyncInterval: undefined,
2020
user: undefined,
2121
lastHash: ''
@@ -41,7 +41,7 @@ async function saveJSONFile(data: object) {
4141
let cleanUpInterval: NodeJS.Timer | undefined = undefined
4242

4343
export const handleCleanUpParameterChange = async () => {
44-
console.log("CLEANUP CALLED")
44+
console.log('CLEANUP CALLED')
4545
if (cleanUpInterval) {
4646
clearInterval(cleanUpInterval)
4747
}
@@ -81,9 +81,9 @@ export const actionsExported = {
8181
},
8282
sendSettings: (settings: IUserPreferences) => actions.sendFrontendNotification(channelsToRender.setSettings, JSON.stringify(settings)),
8383
clearSyncInterval: () => {
84-
console.log("remote sync stopped!")
84+
console.log('remote sync stopped!')
8585
clearInterval(state.remoteSyncInterval)
86-
state.remoteSyncInterval = undefined;
86+
state.remoteSyncInterval = undefined
8787
},
8888
async startRemoteSync(interval: number) {
8989
if (state.remoteSyncInterval) return
@@ -100,7 +100,14 @@ export const actionsExported = {
100100
await ItemRepo.initialLoadItems(state.user.masterKey as string)
101101
state.remoteSyncInterval = setInterval(sync, interval)
102102
},
103-
askPassword: async () => localMainWindow.webContents.send(channelsToRender.askPassword, true),
103+
askPassword: async () => {
104+
const saved = getUserData('savedUser')
105+
if (saved) {
106+
localMainWindow.webContents.send(channelsToRender.askPassword, true)
107+
} else {
108+
localMainWindow.webContents.send(channelsToRender.askPassword, false)
109+
}
110+
},
104111
openWindow: async (window: string) => localMainWindow.webContents.send(channelsToRender.openWindow, window)
105112
}
106113

@@ -118,8 +125,8 @@ const actions = {
118125
localMainWindow.webContents.send(channelsToRender.loadItems, itemList)
119126
},
120127
async startClipboardPooling() {
121-
if (state.pullInterval) return
122-
state.pullInterval = setInterval(async () => await actions.TrySaveClipboard(10), 200)
128+
if (state.clipboardPullInterval) return
129+
state.clipboardPullInterval = setInterval(async () => await actions.TrySaveClipboard(10), 200)
123130
},
124131
async writeToClipboard(hash: string) {
125132
const result = items()?.get(hash)
@@ -197,9 +204,11 @@ const actions = {
197204
logout: () => {
198205
RequestService.account.logout()
199206
actionsExported.clearSyncInterval()
207+
clearInterval(state.clipboardPullInterval)
200208
ItemRepo.reset()
201209
actions.sendItems(ItemRepo.getAll())
202210
state.user = undefined
211+
clearUserData('savedUser')
203212
actionsExported.askPassword()
204213
},
205214
sendFrontendNotification: (notification: typeof channelsToRender[keyof typeof channelsToRender], ...args: any[]) => {
@@ -229,48 +238,77 @@ const ioHookChannels: IReceiveChannel[] = [
229238
}
230239
]
231240

232-
async function loginUser(rawUser: { name: string, password: string, code: string }) {
241+
async function loginUser(rawUser: { name: string; password: string; code: string; wasRemembered: boolean; rememberLogin: boolean }) {
233242
try {
234-
const localUser = CryptoService.HashUserLocal(rawUser)
235-
state.user = localUser
243+
if (rawUser.wasRemembered) {
244+
const data = JSON.parse(getUserData('savedUser')) as ISavedUser
245+
const localUser = CryptoService.HashUserLocal({ name: data.name, password: rawUser.password })
246+
try {
247+
const refresh = CryptoService.DecryptText(data.refresh, localUser.masterKey)
248+
userTokens.refresh = refresh
249+
console.log(refresh)
250+
} catch (error) {
251+
actionsExported.alertFrontend('password incorrect!')
252+
return
253+
}
254+
state.user = localUser
255+
// TODO add retries
256+
const refreshRes = await RequestService.account.refresh()
257+
if (refreshRes.ok && refreshRes.data) {
258+
userTokens.access_expires = DateTime.now().plus({ minutes: 55 })
259+
userTokens.access = refreshRes.data.access_token
260+
} else {
261+
actionsExported.alertFrontend('failed refresh the token. please try to log in again.')
262+
actions.logout()
263+
}
264+
} else {
265+
const localUser = CryptoService.HashUserLocal(rawUser)
266+
state.user = localUser
236267

237-
const loginRes = await RequestService.account.login(localUser.name, localUser.remotePassword, rawUser.code)
268+
const loginRes = await RequestService.account.login(localUser.name, localUser.remotePassword, rawUser.code)
238269

239-
if (loginRes.code === 206) {
240-
if (loginRes.data) {
241-
localMainWindow.webContents.send(channelsToRender.confirmTotp, loginRes.data)
270+
if (loginRes.code === 206) {
271+
if (loginRes.data) {
272+
localMainWindow.webContents.send(channelsToRender.confirmTotp, loginRes.data)
273+
}
274+
return
275+
}
276+
if (loginRes.code === 400) {
277+
actionsExported.alertFrontend('2fa code incorrect!')
278+
return
279+
}
280+
if (loginRes.ok && loginRes.data) {
281+
userTokens.access_expires = DateTime.now().plus({ minutes: 55 })
282+
userTokens.access = loginRes.data.access_token
283+
userTokens.refresh = loginRes.data.refresh_token
284+
if (rawUser.rememberLogin) {
285+
const saved: ISavedUser = {
286+
refresh: CryptoService.EncryptText(userTokens.refresh, state.user.masterKey as string),
287+
name: localUser.name
288+
}
289+
storeUserData('savedUser', JSON.stringify(saved))
290+
}
291+
} else {
292+
actionsExported.alertFrontend('password incorrect!')
242293
}
243-
return
244-
}
245-
if(loginRes.code === 400)
246-
{
247-
actionsExported.alertFrontend("2fa code incorrect!")
248-
return
249294
}
250-
if (loginRes.ok && loginRes.data) {
251-
userTokens.access_expires = DateTime.now().plus({ minutes: 55 })
252-
userTokens.access = loginRes.data.access_token
253-
userTokens.refresh = loginRes.data.refresh_token
254295

255-
actions.startClipboardPooling()
256-
if (userPreferences.enableRemoteSync.value) {
257-
actionsExported.startRemoteSync(userPreferences.remoteSyncInterval.value * 1000)
258-
}
259-
actionsExported.sendCurrentItems()
260-
actionsExported.sendSettings(userPreferences)
261-
actions.sendShortcuts()
262-
actions.sendFrontendNotification(channelsToRender.passwordConfirmed)
263-
} else {
264-
// actions.sendFrontendNotification(channelsToRender.passwordIncorrect)
265-
actionsExported.alertFrontend("password incorrect!")
296+
actions.startClipboardPooling()
297+
if (userPreferences.enableRemoteSync.value) {
298+
actionsExported.startRemoteSync(userPreferences.remoteSyncInterval.value * 1000)
266299
}
300+
actionsExported.sendCurrentItems()
301+
actionsExported.sendSettings(userPreferences)
302+
actions.sendShortcuts()
303+
actions.sendFrontendNotification(channelsToRender.passwordConfirmed)
304+
console.log('end of flow')
267305
} catch (e: any) {
268306
actionsExported.alertFrontend(e.toString())
269307
console.log(e)
270308
}
271309
}
272310

273-
async function registerUser(rawUser: { name: string, password: string }) {
311+
async function registerUser(rawUser: { name: string; password: string }) {
274312
try {
275313
const localUser = CryptoService.HashUserLocal(rawUser)
276314
state.user = localUser
@@ -308,7 +346,7 @@ const channelsFromRender: IReceiveChannel[] = [
308346
{
309347
name: 'to.backend.get.shortcuts',
310348
handler: () => {
311-
console.log("got shortcuts")
349+
console.log('got shortcuts')
312350
actions.sendShortcuts()
313351
}
314352
},
@@ -344,7 +382,7 @@ const channelsFromRender: IReceiveChannel[] = [
344382
name: 'to.backend.text.paste',
345383
handler: async (event: IpcMainEvent, text: string) => {
346384
localClipboard.writeText(text)
347-
actionsExported.alertFrontend("copied!")
385+
actionsExported.alertFrontend('copied!')
348386
}
349387
},
350388
{
@@ -413,7 +451,11 @@ const channelsFromRender: IReceiveChannel[] = [
413451
if (resDeleteItems.ok && resDeleteAccount.ok) {
414452
actionsExported.alertFrontend(messages().accountAndDataDeleted.ok)
415453
} else {
416-
actionsExported.alertFrontend(`${messages().dataDeleted.fail}.\n\nAccount deletion code: ${resDeleteAccount.code}.\nData deletion code: ${resDeleteItems.code}`)
454+
actionsExported.alertFrontend(
455+
`${messages().dataDeleted.fail}.\n\nAccount deletion code: ${resDeleteAccount.code}.\nData deletion code: ${
456+
resDeleteItems.code
457+
}`
458+
)
417459
}
418460
}
419461
},
@@ -430,13 +472,19 @@ const channelsFromRender: IReceiveChannel[] = [
430472
handler: async (event: any, code: string, token: string) => {
431473
const res = await RequestService.account.confirm2fa(code, token)
432474
if (res.ok) {
433-
actionsExported.alertFrontend("2FA successfully enabled! Try logging in")
434-
actionsExported.openWindow("login")
475+
actionsExported.alertFrontend('2FA successfully enabled! Try logging in')
476+
actionsExported.openWindow('login')
435477
} else {
436478
actionsExported.alertFrontend(`failed enabling 2FA. Code ${res.code}`)
437479
}
438480
}
439481
},
482+
{
483+
name: 'to.backend.frontendHandler.isReady',
484+
handler: async (event: any) => {
485+
actionsExported.askPassword()
486+
}
487+
}
440488
]
441489

442490
/**
@@ -454,7 +502,7 @@ export const channelsToRender = {
454502
alert: 'to.renderer.alert',
455503
openWindow: 'to.renderer.open.window',
456504
log: 'to.renderer.log',
457-
confirmTotp: 'to.renderer.confirm.totp',
505+
confirmTotp: 'to.renderer.confirm.totp'
458506
} as const
459507

460508
let localClipboard: Clipboard

src/electron/App/UserPreferences.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ export interface IUserPreferences {
4141

4242
export const store = new Store()
4343

44+
const getDataKey = (key: string) => `${AppSettings.name}.data.${key}`
45+
46+
export const UserDataKeys = {
47+
savedUser: 'savedUser'
48+
}
49+
50+
export function storeUserData(key: keyof typeof UserDataKeys, data: string) {
51+
store.set(getDataKey(key), data)
52+
}
53+
export function getUserData(key: keyof typeof UserDataKeys) {
54+
return store.get(getDataKey(key)) as string
55+
}
56+
export function clearUserData(key: keyof typeof UserDataKeys) {
57+
store.delete(getDataKey(key))
58+
}
4459
/**
4560
* Used to generate the key for the electron-storage preference saving
4661
*/
@@ -183,7 +198,7 @@ export const userPreferences: IUserPreferences = {
183198
authUrl: {
184199
displayName: 'Auth server URL',
185200
description: 'the server used for authentication',
186-
value: "https://dev.auth.fireclip.net",
201+
value: 'https://dev.auth.fireclip.net',
187202
type: 'string',
188203
selectableOptions: undefined,
189204
changeHandler: (e, event) => {
@@ -195,7 +210,7 @@ export const userPreferences: IUserPreferences = {
195210
storeUrl: {
196211
displayName: 'Store server URL',
197212
description: 'server used for clips storage',
198-
value: "https://dev.clips.fireclip.net",
213+
value: 'https://dev.clips.fireclip.net',
199214
type: 'string',
200215
selectableOptions: undefined,
201216
changeHandler: (e, event) => {
@@ -228,7 +243,7 @@ export const userPreferences: IUserPreferences = {
228243
userPreferences.enableRemoteSync.value = event.value
229244
defaultHandler(e, event)
230245
}
231-
},
246+
}
232247
}
233248

234249
/**
@@ -285,6 +300,5 @@ export const InitUserSettings = async () => {
285300
}
286301

287302
// default triggering shortcut, always enabled (most likely also a user settings in the future)
288-
globalShortcut.register('CommandOrControl+`', () => {
289-
})
303+
globalShortcut.register('CommandOrControl+`', () => {})
290304
}

src/electron/DataModels/LocalTypes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface IKeyboardEvent {
1212

1313
export interface IAppState {
1414
user: ILocalUser | undefined
15-
pullInterval: NodeJS.Timer | undefined
15+
clipboardPullInterval: NodeJS.Timer | undefined
1616
remoteSyncInterval: NodeJS.Timer | undefined
1717
// index: number
1818
// ctrlA: boolean
@@ -22,6 +22,11 @@ export interface IAppState {
2222
lastHash: string
2323
}
2424

25+
export interface ISavedUser {
26+
name: string
27+
refresh: string
28+
}
29+
2530
export interface IReceiveChannel {
2631
name: string
2732
handler: (event: IpcMainEvent, ...args: any[]) => Promise<void> | void

src/frontend/Components/EventHandler.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { get } from 'svelte/store'
33
import { arrayToArrayMap, getKeyName, ioHook, itemMatchesText, sort } from '../KeyboardEventUtil'
4-
import { ipcRenderer } from '../events'
4+
import { events, eventsToBackend, ipcRenderer } from '../events'
55
import {
66
backendLogs,
77
clipList,
@@ -11,6 +11,7 @@
1111
isAppHidden,
1212
isPasswordAsked,
1313
isPasswordIncorrect,
14+
isUserRemembered,
1415
pressedKeys,
1516
pressedKeysSizeLimit,
1617
searchOnlyImages,
@@ -20,6 +21,11 @@
2021
} from '../stores'
2122
import type { IClipboardItem, IHookKeyboardEvent, IReceiveChannel } from '../types'
2223
import { IPages, isImageContent } from '../types'
24+
import { onMount } from 'svelte'
25+
26+
onMount(() => {
27+
events.notifyBackend(eventsToBackend.frontendEventsReady)
28+
})
2329
2430
currentSearchedText.subscribe((text: string) => {
2531
if ($searchOnlyImages) {
@@ -70,6 +76,7 @@
7076
{
7177
name: 'to.renderer.askPassword',
7278
handler: function (event, store) {
79+
isUserRemembered.set(store)
7380
$isPasswordAsked = true
7481
$currentPage = IPages.login
7582
}
@@ -95,8 +102,8 @@
95102
{
96103
name: 'to.renderer.passwordConfirmed',
97104
handler: function (event) {
98-
$isPasswordAsked = false
99-
$currentPage = IPages.items
105+
isPasswordAsked.set(false)
106+
currentPage.set(IPages.items)
100107
}
101108
},
102109
{

0 commit comments

Comments
 (0)