Skip to content

Commit 4e343c4

Browse files
feat: nethcti-middleware (#72)
1 parent 7a27e75 commit 4e343c4

File tree

28 files changed

+1241
-351
lines changed

28 files changed

+1241
-351
lines changed

package-lock.json

Lines changed: 8 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands",
5050
"@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light",
5151
"@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid",
52-
"@nethesis/phone-island": "^0.15.11",
52+
"@nethesis/phone-island": "^0.17.4",
5353
"@tailwindcss/forms": "^0.5.7",
5454
"@types/lodash": "^4.14.202",
5555
"@types/node": "^18.19.9",

public/locales/en/translations.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,16 @@
109109
"Account List title": "Account list",
110110
"Account List description": "Choose an account to continue to NethLink.",
111111
"Delete account": "Are you sure you want to delete {{username}}?",
112-
"Back": "Back"
112+
"Back": "Back",
113+
"User not authorized for NethLink": "User not authorized for NethLink",
114+
"Generic error": "Generic error",
115+
"2FA": {
116+
"OTP code": "OTP code",
117+
"OTP invalid": "The code you entered is invalid or has expired. Please check your authenticator app and try again.",
118+
"OTP verification failed": "OTP verification failed",
119+
"Two-Factor Authentication": "Two-Factor Authentication",
120+
"Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code."
121+
}
113122
},
114123
"SplashScreen": {
115124
"Description": "Welcome to NethLink, a desktop solution for seamless communication. Make and receive calls, save contacts to you phonebook and much more.",

public/locales/it/translations.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,16 @@
109109
"Account List title": "Account disponibili",
110110
"Account List description": "Scegliete un account per proseguire con NethLink.",
111111
"Delete account": "Sei sicuro di voler eliminare {{username}}?",
112-
"Back": "Indietro"
112+
"Back": "Indietro",
113+
"User not authorized for NethLink": "Utente non autorizzato per NethLink",
114+
"Generic error": "Errore generico",
115+
"2FA": {
116+
"OTP code": "Codice OTP",
117+
"OTP invalid": "Il codice inserito non è valido o è scaduto. Controlla la tua app di autenticazione e riprova.",
118+
"OTP verification failed": "Verifica OTP fallita",
119+
"Two-Factor Authentication": "Autenticazione a Due Fattori",
120+
"Enter the 6-digit code (OTP code) from your authenticator app. If you cannot access the app, you can use one recovery OTP code.": "Inserisci il codice a 6 cifre (codice OTP) dalla tua app di autenticazione. Se non puoi accedere all'app, puoi utilizzare un codice OTP di recupero."
121+
}
113122
},
114123
"SplashScreen": {
115124
"Description": "Benvenuti in NethLink, la soluzione desktop per comunicazioni senza confini. Effettua e ricevi chiamate, salva i contatti nella tua rubrica e molto altro ancora.",

src/main/classes/controllers/AccountController.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { store } from '@/lib/mainStore'
55
import { useNethVoiceAPI } from '@shared/useNethVoiceAPI'
66
import { useLogin } from '@shared/useLogin'
77
import { NetworkController } from './NetworkController'
8-
import { delay, getAccountUID } from '@shared/utils/utils'
8+
import { getAccountUID } from '@shared/utils/utils'
9+
import { requires2FA, isJWTExpired } from '@shared/utils/jwt'
910

1011
const defaultConfig: ConfigFile = {
1112
lastUser: undefined,
@@ -76,7 +77,75 @@ export class AccountController {
7677
const decryptString = safeStorage.decryptString(psw)
7778
const _accountData = JSON.parse(decryptString)
7879
const password = _accountData.password
80+
81+
// Check if saved token is still valid and doesn't require 2FA
82+
if (lastLoggedAccount.jwtToken) {
83+
if (!isJWTExpired(lastLoggedAccount.jwtToken)) {
84+
// Token is still valid locally, check if it requires 2FA
85+
if (requires2FA(lastLoggedAccount.jwtToken)) {
86+
Log.info('auto login failed: 2FA required, user interaction needed')
87+
return false
88+
}
89+
90+
// Token looks valid locally, but we need to verify with server
91+
// The token might have been invalidated (e.g., 2FA disabled/enabled)
92+
Log.info('auto login: validating saved token with server...')
93+
94+
try {
95+
// Make a simple API call to verify the token is still accepted by the server
96+
// Use the /api/user/me endpoint to validate the token
97+
const testUrl = `https://${lastLoggedAccount.host}/api/user/me`
98+
const response = await NetworkController.instance.get(testUrl, {
99+
headers: {
100+
'Authorization': `Bearer ${lastLoggedAccount.jwtToken}`
101+
}
102+
} as any)
103+
104+
// If we get here, the token is valid on the server
105+
Log.info('auto login: token validated with server, using saved token')
106+
} catch (error: any) {
107+
// Token was rejected by server (401/403) or network error
108+
Log.info('auto login failed: saved token rejected by server', error?.response?.status || error?.message)
109+
return false
110+
}
111+
112+
// Update store with the saved account (don't do a new login!)
113+
// IMPORTANT: Preserve auth.lastUser and auth.lastUserCryptPsw so they are saved to disk
114+
// IMPORTANT: Set connection: true to prevent "No internet connection" banner
115+
store.updateStore({
116+
account: lastLoggedAccount,
117+
theme: lastLoggedAccount.theme,
118+
connection: true,
119+
accountStatus: store.store.accountStatus || 'offline',
120+
isCallsEnabled: store.store.isCallsEnabled || false,
121+
auth: {
122+
...authAppData,
123+
lastUser: authAppData.lastUser,
124+
lastUserCryptPsw: authAppData.lastUserCryptPsw
125+
}
126+
}, 'autoLogin')
127+
128+
return true
129+
} else {
130+
Log.info('auto login: saved token expired, need to re-login')
131+
}
132+
}
133+
134+
// Token is expired or doesn't exist, do a new login
79135
const tempLoggedAccount = await this.NethVoiceAPI.Authentication.login(lastLoggedAccount.host, lastLoggedAccount.username, password)
136+
137+
// Check if 2FA is required - auto-login should fail in this case
138+
if (tempLoggedAccount.jwtToken && requires2FA(tempLoggedAccount.jwtToken)) {
139+
Log.info('auto login failed: 2FA required, user interaction needed')
140+
return false
141+
}
142+
143+
// Auto-login only works with JWT tokens (no legacy support)
144+
if (!tempLoggedAccount.jwtToken) {
145+
Log.info('auto login failed: no JWT token received')
146+
return false
147+
}
148+
80149
let loggedAccount: Account = {
81150
...lastLoggedAccount,
82151
...tempLoggedAccount,

src/main/classes/controllers/PhoneIslandController.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Extension, Size } from '@shared/types'
1010
export class PhoneIslandController {
1111
static instance: PhoneIslandController
1212
window: PhoneIslandWindow
13+
private isWarmingUp: boolean = false
1314

1415
constructor() {
1516
PhoneIslandController.instance = this
@@ -30,7 +31,8 @@ export class PhoneIslandController {
3031
if (h === 0 && w === 0) {
3132
window.hide()
3233
} else {
33-
if (!window.isVisible()) {
34+
// Don't show window during warm-up
35+
if (!window.isVisible() && !this.isWarmingUp) {
3436
window.show()
3537
window.setAlwaysOnTop(true)
3638
}
@@ -148,6 +150,60 @@ export class PhoneIslandController {
148150
}
149151
}
150152

153+
muteAudio() {
154+
try {
155+
const window = this.window.getWindow()
156+
if (window && window.webContents) {
157+
window.webContents.setAudioMuted(true)
158+
Log.info('PhoneIsland audio muted')
159+
}
160+
} catch (e) {
161+
Log.warning('error during muting PhoneIsland audio:', e)
162+
}
163+
}
164+
165+
unmuteAudio() {
166+
try {
167+
const window = this.window.getWindow()
168+
if (window && window.webContents) {
169+
window.webContents.setAudioMuted(false)
170+
Log.info('PhoneIsland audio unmuted')
171+
}
172+
} catch (e) {
173+
Log.warning('error during unmuting PhoneIsland audio:', e)
174+
}
175+
}
176+
177+
forceHide() {
178+
try {
179+
const window = this.window.getWindow()
180+
if (window) {
181+
this.isWarmingUp = true
182+
window.hide()
183+
Log.info('PhoneIsland window hidden')
184+
}
185+
} catch (e) {
186+
Log.warning('error during force hiding PhoneIsland:', e)
187+
}
188+
}
189+
190+
forceShow() {
191+
try {
192+
const window = this.window.getWindow()
193+
if (window) {
194+
this.isWarmingUp = false
195+
// Only show if there's actually content (size > 0)
196+
const bounds = window.getBounds()
197+
if (bounds.width > 0 && bounds.height > 0) {
198+
window.show()
199+
window.setAlwaysOnTop(true)
200+
Log.info('PhoneIsland window shown')
201+
}
202+
}
203+
} catch (e) {
204+
Log.warning('error during force showing PhoneIsland:', e)
205+
}
206+
}
151207

152208
async safeQuit() {
153209
await this.logout()

src/main/lib/ipcEvents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import os from 'os'
1818

1919
const { keyboard, Key } = require("@nut-tree-fork/nut-js");
2020

21+
// Global flag to ensure audio warm-up runs only once per app session
22+
let hasRunAudioWarmup = false
23+
2124
function onSyncEmitter<T>(
2225
channel: IPC_EVENTS,
2326
asyncCallback: (...args: any[]) => Promise<T>

0 commit comments

Comments
 (0)