Skip to content

Commit 4063db6

Browse files
committed
Fixed Support Profiles + Added encryption to env
1 parent 6b424ff commit 4063db6

File tree

9 files changed

+151
-54
lines changed

9 files changed

+151
-54
lines changed
Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
1-
// Builds environment variables into production .env file
2-
import { writeFileSync } from 'fs'
1+
import { writeFileSync, readFileSync } from 'fs'
32
import path from 'path'
3+
import crypto from 'crypto'
44
import { config } from 'dotenv'
55
import { fileURLToPath } from 'url'
66

77
config()
88

9-
const appConfig = {
10-
discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || ''
11-
}
12-
13-
// Write env file
9+
// Read package.json for app metadata
1410
const _filename = fileURLToPath(import.meta.url)
1511
const __dirname = path.dirname(_filename)
12+
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf8'))
13+
14+
// Create encryption key from app metadata
15+
const keyMaterial = `${packageJson.version}-deskthing-secrets`
16+
const encryptionKey = crypto.scryptSync(keyMaterial, 'dt-salt-v1', 32)
17+
18+
function encryptSecret(secret) {
19+
if (!secret) return ''
20+
21+
const iv = crypto.randomBytes(16)
22+
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv)
23+
24+
let encrypted = cipher.update(secret, 'utf8', 'hex')
25+
encrypted += cipher.final('hex')
26+
27+
return iv.toString('hex') + ':' + encrypted
28+
}
29+
30+
const buildConfig = {
31+
// Encrypted secrets from GitHub Actions
32+
secrets: {
33+
DISCORD_WEBHOOK_URL: encryptSecret(process.env.DISCORD_WEBHOOK_URL),
34+
BUYMEACOFFEE_TOKEN: encryptSecret(process.env.BUYMEACOFFEE_TOKEN)
35+
},
36+
37+
meta: {
38+
hasSecrets: Boolean(process.env.DISCORD_WEBHOOK_URL || process.env.BUYMEACOFFEE_TOKEN)
39+
}
40+
}
41+
1642
const configPath = path.resolve(__dirname, 'src/main/config.json')
43+
writeFileSync(configPath, JSON.stringify(buildConfig, null, 2))
1744

18-
writeFileSync(configPath, JSON.stringify(appConfig, null, 2))
19-
console.log(`Config file written to ${configPath}`)
45+
console.log(`✅ Build config written to ${configPath}`)
46+
console.log(`📦 Secrets included: ${buildConfig.meta.hasSecrets}`)

DeskThingServer/src/main/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { initializationCheck } from './services/initialize'
2424

2525
// Initialize environment variables
2626
import './utils/environment'
27+
import { loadConfig } from './utils/envBuilder'
2728

2829
// Ensure single instance
2930
if (!setupSingleInstance()) {
@@ -39,6 +40,9 @@ if (!setupSingleInstance()) {
3940
// Clears any old installs
4041
await initializationCheck()
4142

43+
// build the environment variables
44+
loadConfig()
45+
4246
// Initialize app lifecycle (which will handle the rest of the startup)
4347
await initializeAppLifecycle()
4448
})

DeskThingServer/src/main/services/feedbackService.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,7 @@ import { storeProvider } from '@server/stores/storeProvider'
33
import { handleError } from '@server/utils/errorHandler'
44
import logger from '@server/utils/logger'
55
import { DiscordWebhookData, FeedbackReport, FeedbackResult, FeedbackType, SystemInfo } from '@shared/types'
6-
import { readFileSync } from 'fs'
76
import os from 'os'
8-
import { join } from 'path'
9-
10-
let appConfig: { discordWebhookUrl: string }
11-
try {
12-
// In development
13-
if (process.env.NODE_ENV === 'development') {
14-
appConfig = { discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || '' }
15-
} else {
16-
// In production - read from bundled config
17-
const configPath = join(process.resourcesPath, 'config.json')
18-
appConfig = JSON.parse(readFileSync(configPath, 'utf8'))
19-
}
20-
} catch (error) {
21-
logger.error('Failed to load config:', { source: 'feedbackService', error: error as Error })
22-
appConfig = { discordWebhookUrl: '' }
23-
}
247

258
export class FeedbackService {
269
private static readonly SUBMISSION_ID = Math.floor(Math.random() * 900) + 100
@@ -29,7 +12,7 @@ export class FeedbackService {
2912
* This will be updated later for an actual webhook edge server smth smth fancy instead of directly to the discord channel
3013
* If you could be a real one and not abuse this, that would be awesome. Thanks!
3114
*/
32-
private static readonly WEBHOOK_URL = appConfig.discordWebhookUrl || 'fallback-webhook-url'
15+
private static readonly WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || 'fallback-webhook-url'
3316

3417
private static readonly TYPE_COLORS: Record<FeedbackType, number> = {
3518
bug: 0xff0000,

DeskThingServer/src/main/stores/platforms/superbird/adbPlatform.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class ADBPlatform extends EventEmitter<PlatformEvents> implements Platfor
4545
connectionState: ConnectionState.Established
4646
}
4747

48-
4948
constructor() {
5049
super()
5150
this.adbService = new ADBService()

DeskThingServer/src/main/stores/supporterStore.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { CacheableStore } from '@shared/types'
22
import logger from '@server/utils/logger'
3-
import { readFileSync } from 'fs'
4-
import { join } from 'path'
53
import {
64
Supporter,
75
SupporterAPIResponse,
@@ -19,26 +17,12 @@ type SupporterCachePage = {
1917

2018
type SupporterCache = Record<number, SupporterCachePage>
2119

22-
let appConfig: { buyMeACoffeeToken: string }
23-
24-
try {
25-
if (process.env.NODE_ENV === 'development') {
26-
appConfig = { buyMeACoffeeToken: process.env.BUYMEACOFFE_TOKEN || '' }
27-
} else {
28-
const configPath = join(process.resourcesPath, 'config.json')
29-
appConfig = JSON.parse(readFileSync(configPath, 'utf8'))
30-
}
31-
} catch (error) {
32-
logger.error('Failed to load config:', { source: 'supporterStore', error: error as Error })
33-
appConfig = { buyMeACoffeeToken: '' }
34-
}
35-
3620
export class SupporterStore implements SupporterStoreClass, CacheableStore {
3721
private cache: SupporterCache | null = null
38-
private readonly CACHE_DURATION = 2 * 60 * 60 * 1000 // 2 hours
22+
private readonly CACHE_DURATION = 1 * 60 * 60 * 1000 // 1 hour
3923
private readonly API_URL = 'https://developers.buymeacoffee.com/api/v1/supporters'
4024
private readonly MEMBERS_API_URL = 'https://developers.buymeacoffee.com/api/v1/subscriptions'
41-
private readonly TOKEN = appConfig.buyMeACoffeeToken
25+
private readonly TOKEN = process.env.BUYMEACOFFEE_TOKEN
4226

4327
private _initialized: boolean = false
4428
public get initialized(): boolean {
@@ -68,7 +52,7 @@ export class SupporterStore implements SupporterStoreClass, CacheableStore {
6852
})
6953

7054
if (!response.ok) {
71-
throw new Error(`Failed to fetch supporters: ${response.statusText}`)
55+
throw new Error(`Failed to fetch supporters: ${response.status} - ${response.statusText}`)
7256
}
7357

7458
const data = (await response.json()) as SupporterAPIResponse
@@ -97,7 +81,10 @@ export class SupporterStore implements SupporterStoreClass, CacheableStore {
9781
})
9882

9983
if (!response.ok) {
100-
throw new Error(`Failed to fetch members: ${response.statusText}`)
84+
logger.debug(
85+
`Failed to fetch members. Token Exists: ${!!this.TOKEN}. Api Url Exists: ${this.MEMBERS_API_URL}`
86+
)
87+
throw new Error(`Failed to fetch members: ${response.statusText} (${response.status})`)
10188
}
10289

10390
const data = (await response.json()) as MemberAPIResponse
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { app } from 'electron/main'
2+
import crypto from 'crypto'
3+
import { join } from 'path'
4+
import { existsSync, readFileSync } from 'fs'
5+
import { config } from 'dotenv'
6+
7+
config()
8+
9+
type Secrets = Record<string, string>
10+
11+
interface AppConfig {
12+
secrets: Secrets
13+
meta?: {
14+
hasSecrets?: boolean
15+
}
16+
}
17+
18+
let configuration: AppConfig | null = null
19+
let decryptedSecrets: Secrets = {}
20+
21+
function createDecryptionKey(): Buffer {
22+
// Must match the salt and keyMaterial from electron-builder.env.js
23+
const keyMaterial = `${app.getVersion()}-deskthing-secrets`
24+
return crypto.scryptSync(keyMaterial, 'dt-salt-v1', 32)
25+
}
26+
27+
function decrypt(encryptedData: string): string {
28+
if (!encryptedData) return ''
29+
try {
30+
const key = createDecryptionKey()
31+
const [ivHex, encrypted] = encryptedData.split(':')
32+
const iv = Buffer.from(ivHex, 'hex')
33+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
34+
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
35+
decrypted += decipher.final('utf8')
36+
return decrypted
37+
} catch (error) {
38+
console.error('Failed to decrypt secret:', error)
39+
return ''
40+
}
41+
}
42+
43+
function addSecretsToEnv(): void {
44+
if (!configuration || !configuration.secrets) return
45+
46+
if (!decryptedSecrets) {
47+
decryptSecrets()
48+
}
49+
50+
for (const [key, value] of Object.entries(decryptedSecrets)) {
51+
process.env[key] = value
52+
}
53+
}
54+
55+
function loadConfig(): void {
56+
try {
57+
const configPath = join(process.resourcesPath, 'config.json')
58+
if (existsSync(configPath)) {
59+
const configData = readFileSync(configPath, 'utf8')
60+
configuration = JSON.parse(configData) as AppConfig
61+
decryptSecrets()
62+
addSecretsToEnv()
63+
} else {
64+
console.warn('No config file found, using empty secrets')
65+
configuration = { secrets: {} }
66+
decryptedSecrets = {}
67+
}
68+
} catch (error) {
69+
console.error('Failed to load config:', error)
70+
configuration = { secrets: {} }
71+
decryptedSecrets = {}
72+
}
73+
}
74+
75+
function decryptSecrets(): void {
76+
if (!configuration) return
77+
decryptedSecrets = {}
78+
for (const [key, value] of Object.entries(configuration.secrets)) {
79+
decryptedSecrets[key] = decrypt(value)
80+
}
81+
}
82+
83+
function getSecret(key: string): string {
84+
return decryptedSecrets[key] || ''
85+
}
86+
87+
function hasSecret(key: string): boolean {
88+
return Boolean(decryptedSecrets[key])
89+
}
90+
91+
function getConfig(): AppConfig | null {
92+
return configuration
93+
}
94+
95+
// Immediately load config on import
96+
loadConfig()
97+
98+
export { getSecret, hasSecret, getConfig, loadConfig }

DeskThingServer/src/renderer/public/images/authors/riprod.gif renamed to DeskThingServer/src/renderer/src/assets/images/authors/riprod.gif

File renamed without changes.

DeskThingServer/src/renderer/public/images/authors/thebigloud.webp renamed to DeskThingServer/src/renderer/src/assets/images/authors/thebigloud.webp

File renamed without changes.

DeskThingServer/src/renderer/src/overlays/settings/About.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import Button from '@renderer/components/Button'
33
import { Member, Supporter } from '@shared/types/supporter'
44
import User from '@renderer/components/supporter/User'
55

6+
import riprodAvatar from '@renderer/assets/images/authors/riprod.gif'
7+
import thebigloudAvatar from '@renderer/assets/images/authors/thebigloud.webp'
8+
69
const AboutSettings: React.FC = () => {
710
const [supporters, setSupporters] = useState<{ monthly: Member[]; onetime: Supporter[] }>({
811
monthly: [],
@@ -48,15 +51,11 @@ const AboutSettings: React.FC = () => {
4851
<div>
4952
<h1 className="text-lg">Development Team</h1>
5053
<User
51-
avatar="/images/authors/riprod.gif"
54+
avatar={riprodAvatar}
5255
name="Riprod"
5356
contribution="Lead Developer & Project Maintainer"
5457
/>
55-
<User
56-
avatar="/images/authors/thebigloud.webp"
57-
name="TheBigLoud"
58-
contribution="UI Designer"
59-
/>
58+
<User avatar={thebigloudAvatar} name="TheBigLoud" contribution="UI Designer" />
6059
</div>
6160

6261
<div className="flex md:flex-row flex-col gap-4">

0 commit comments

Comments
 (0)