Skip to content

Commit 3820415

Browse files
committed
feat(studio): enhance security and stability across core services
- Implement cryptographically secure random string generation using `crypto.getRandomValues` for filenames and session IDs. - Add `Suspense` boundary to `StudioRedirectPage` to handle client-side navigation requirements. - Improve server-side security by validating and sanitizing Firebase Storage URLs and Printerz template IDs. - Refactor `AudioPlayer` playback logic for better guard clauses and error handling. - Update `persistence-service` to use `globalThis.indexedDB` for improved environment compatibility. - Optimize data processing in `firebase-utils` using `codePointAt` for safer character handling.
1 parent 3c9017a commit 3820415

6 files changed

Lines changed: 57 additions & 34 deletions

File tree

src/app/studio/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import { FileAudio } from "lucide-react"
44
import { useRouter, useSearchParams } from "next/navigation"
5+
import { Suspense } from "react"
56
import { Button } from "@/components/ui/button"
67

7-
export default function StudioRedirectPage() {
8+
function StudioRedirectContent() {
89
const router = useRouter()
910
const searchParams = useSearchParams()
1011

@@ -35,3 +36,11 @@ export default function StudioRedirectPage() {
3536
</div>
3637
)
3738
}
39+
40+
export default function StudioRedirectPage() {
41+
return (
42+
<Suspense>
43+
<StudioRedirectContent />
44+
</Suspense>
45+
)
46+
}

src/components/studio/AudioPlayer.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -237,19 +237,18 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
237237
}, [currentTime, isLooping, loopStart, loopEnd, audioRef])
238238

239239
const togglePlay = () => {
240-
if (audioRef.current) {
241-
if (isPlaying) {
242-
audioRef.current.pause()
243-
setIsPlaying(false)
244-
} else {
245-
audioRef.current
246-
.play()
247-
.then(() => setIsPlaying(true))
248-
.catch((error) => {
249-
console.error("Audio playback failed:", error)
250-
toast.error("Failed to play audio")
251-
})
252-
}
240+
if (!audioRef.current) return
241+
if (isPlaying) {
242+
audioRef.current.pause()
243+
setIsPlaying(false)
244+
} else {
245+
audioRef.current
246+
.play()
247+
.then(() => setIsPlaying(true))
248+
.catch((error) => {
249+
console.error("Audio playback failed:", error)
250+
toast.error("Failed to play audio")
251+
})
253252
}
254253
}
255254

src/lib/firebase-utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { getStorage } from "@/lib/firebase"
33

44
const generateUniqueFilename = (originalName: string): string => {
55
const timestamp = Date.now()
6-
const randomString = Math.random().toString(36).substring(2, 8)
6+
const randomBytes = new Uint8Array(4)
7+
crypto.getRandomValues(randomBytes)
8+
const randomString = Array.from(randomBytes)
9+
.map((b) => b.toString(16).padStart(2, "0"))
10+
.join("")
711
const fileExtension = originalName?.split(".").pop() || "audio"
812
return `audio_${timestamp}_${randomString}.${fileExtension}`
913
}
@@ -26,7 +30,7 @@ export async function uploadBase64ToFirebase(
2630
const byteCharacters = atob(base64WithoutPrefix)
2731
const byteNumbers = new Array(byteCharacters.length)
2832
for (let i = 0; i < byteCharacters.length; i++) {
29-
byteNumbers[i] = byteCharacters.charCodeAt(i)
33+
byteNumbers[i] = byteCharacters.codePointAt(i) ?? 0
3034
}
3135
const byteArray = new Uint8Array(byteNumbers)
3236

src/lib/persistence-service.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@
44
*/
55

66
import { TranscriptionStatus } from "@/services/transcription"
7-
import type {
8-
TranscriptionIntelligence,
9-
TranscriptionSegment,
10-
} from "@/types/transcription"
7+
import type { TranscriptionIntelligence } from "@/types/transcription"
118

12-
export type { TranscriptionSegment }
9+
export type { TranscriptionSegment } from "@/types/transcription"
1310

1411
export interface TranscriptionSession {
1512
id: string // Unique session ID
@@ -55,13 +52,13 @@ const DEFAULT_SESSION_EXPIRY_HOURS = 24
5552
const initDb = (): Promise<IDBDatabase> => {
5653
return new Promise((resolve, reject) => {
5754
// Check for IndexedDB support
58-
if (!window.indexedDB) {
55+
if (!globalThis.indexedDB) {
5956
console.error("Your browser doesn't support IndexedDB")
6057
reject(new Error("IndexedDB not supported"))
6158
return
6259
}
6360

64-
const request = window.indexedDB.open(DB_NAME, DB_VERSION)
61+
const request = globalThis.indexedDB.open(DB_NAME, DB_VERSION)
6562

6663
request.onerror = (event) => {
6764
console.error("Database error:", event)
@@ -94,7 +91,11 @@ const initDb = (): Promise<IDBDatabase> => {
9491
*/
9592
export const createSessionId = (): string => {
9693
const timestamp = Date.now()
97-
const randomString = Math.random().toString(36).substring(2, 10)
94+
const randomBytes = new Uint8Array(5)
95+
crypto.getRandomValues(randomBytes)
96+
const randomString = Array.from(randomBytes)
97+
.map((b) => b.toString(16).padStart(2, "0"))
98+
.join("")
9899
const sessionId = `${timestamp}-${randomString}`
99100

100101
// Set session cookie

src/lib/storage-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const deleteFile = async (path: string): Promise<void> => {
5757
if (path.startsWith("http")) {
5858
// Try to convert URL to storage path - this is tricky and implementation depends on URL format
5959
// For simplicity, if path contains 'temp_audio/', extract that part and everything after
60-
const match = path.match(/temp_audio\/.+/)
60+
const match = /temp_audio\/.+/.exec(path)
6161
if (match) {
6262
filePath = match[0]
6363
} else {

src/server/index.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,11 @@ app.post("/api/printerz/render", (async (req: Request, res: Response) => {
152152
return res.status(500).json({ error: "Printerz API key not configured" })
153153
}
154154

155-
console.log(`Proxying request to Printerz template: ${templateId}`)
156-
console.log(
157-
"With data:",
158-
JSON.stringify(printerzData).substring(0, 200) + "...",
159-
)
155+
console.log("Proxying request to Printerz")
160156

157+
const safeTemplateId = encodeURIComponent(String(templateId))
161158
const response = await fetch(
162-
`https://api.printerz.dev/templates/${templateId}/render`,
159+
`https://api.printerz.dev/templates/${safeTemplateId}/render`,
163160
{
164161
method: "POST",
165162
headers: {
@@ -213,15 +210,28 @@ app.post("/api/firebase-proxy", (async (req: Request, res: Response) => {
213210
try {
214211
const { url } = req.body
215212

216-
if (!url || !url.includes("firebasestorage.googleapis.com")) {
213+
if (!url || typeof url !== "string") {
214+
return res
215+
.status(400)
216+
.json({ error: "Invalid or missing Firebase Storage URL" })
217+
}
218+
219+
let parsedUrl: URL
220+
try {
221+
parsedUrl = new URL(url)
222+
} catch {
223+
return res.status(400).json({ error: "Malformed Firebase Storage URL" })
224+
}
225+
226+
if (parsedUrl.hostname !== "firebasestorage.googleapis.com") {
217227
return res
218228
.status(400)
219229
.json({ error: "Invalid or missing Firebase Storage URL" })
220230
}
221231

222-
console.log(`Proxying request to Firebase Storage: ${url}`)
232+
console.log("Proxying request to Firebase Storage")
223233

224-
const response = await fetch(url)
234+
const response = await fetch(parsedUrl.toString())
225235

226236
if (!response.ok) {
227237
return res.status(response.status).json({

0 commit comments

Comments
 (0)