diff options
Diffstat (limited to 'frontend/lib')
| -rw-r--r-- | frontend/lib/auth.ts | 103 | ||||
| -rw-r--r-- | frontend/lib/drive.ts | 91 | ||||
| -rw-r--r-- | frontend/lib/env.ts | 3 | ||||
| -rw-r--r-- | frontend/lib/utils.ts | 26 |
4 files changed, 223 insertions, 0 deletions
diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts new file mode 100644 index 0000000..fe00b11 --- /dev/null +++ b/frontend/lib/auth.ts | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | import { cookies } from 'next/headers'; | ||
| 2 | import { Env_is_development } from './env'; | ||
| 3 | import { Elsie } from 'next/font/google'; | ||
| 4 | |||
| 5 | export interface UserSessionCookie { | ||
| 6 | name: string, | ||
| 7 | value: string, | ||
| 8 | } | ||
| 9 | |||
| 10 | export interface UserAuth { | ||
| 11 | isLoggedIn: boolean, | ||
| 12 | username: string, | ||
| 13 | name: string, | ||
| 14 | email: string, | ||
| 15 | provider: string, | ||
| 16 | oauth: boolean, | ||
| 17 | } | ||
| 18 | |||
| 19 | export async function Auth_extract_session_cookie(): Promise<UserSessionCookie | null> { | ||
| 20 | const cookieStore = await cookies(); | ||
| 21 | for (const cookie of cookieStore.getAll()) { | ||
| 22 | if (!cookie.name.includes("tinyauth-session")) | ||
| 23 | continue; | ||
| 24 | return { | ||
| 25 | name: cookie.name, | ||
| 26 | value: cookie.value, | ||
| 27 | } as UserSessionCookie; | ||
| 28 | } | ||
| 29 | return null; | ||
| 30 | } | ||
| 31 | |||
| 32 | export async function Auth_get_user(): Promise<UserAuth> { | ||
| 33 | const cookie = await Auth_extract_session_cookie(); | ||
| 34 | const endpoint = Auth_tinyauth_endpoint(); | ||
| 35 | |||
| 36 | try { | ||
| 37 | const headers: Record<string, string> = {}; | ||
| 38 | if (cookie) { | ||
| 39 | headers['Cookie'] = `${cookie.name}=${cookie.value}`; | ||
| 40 | } | ||
| 41 | |||
| 42 | const response = await fetch(`${endpoint}/api/user`, { | ||
| 43 | method: 'GET', | ||
| 44 | headers | ||
| 45 | }); | ||
| 46 | |||
| 47 | if (!response.ok) { | ||
| 48 | return { | ||
| 49 | isLoggedIn: false, | ||
| 50 | username: '', | ||
| 51 | name: '', | ||
| 52 | email: '', | ||
| 53 | provider: '', | ||
| 54 | oauth: false | ||
| 55 | }; | ||
| 56 | } | ||
| 57 | |||
| 58 | const data = await response.json(); | ||
| 59 | |||
| 60 | return { | ||
| 61 | isLoggedIn: data.isLoggedIn || false, | ||
| 62 | username: data.username || '', | ||
| 63 | name: data.name || '', | ||
| 64 | email: data.email || '', | ||
| 65 | provider: data.provider || '', | ||
| 66 | oauth: data.oauth || false | ||
| 67 | }; | ||
| 68 | } catch (error) { | ||
| 69 | console.error('Failed to fetch user:', error); | ||
| 70 | return { | ||
| 71 | isLoggedIn: false, | ||
| 72 | username: '', | ||
| 73 | name: '', | ||
| 74 | email: '', | ||
| 75 | provider: '', | ||
| 76 | oauth: false | ||
| 77 | }; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | export function Auth_user_can_upload(user: UserAuth): boolean { | ||
| 82 | if (!user.isLoggedIn) | ||
| 83 | return false; | ||
| 84 | |||
| 85 | if (Env_is_development()) | ||
| 86 | return true; | ||
| 87 | |||
| 88 | return user.oauth && user.email.endsWith("@campus.fct.unl.pt"); | ||
| 89 | } | ||
| 90 | |||
| 91 | function Auth_tinyauth_endpoint(): string { | ||
| 92 | const endpoint = process.env.TINYAUTH_ENDPOINT; | ||
| 93 | if (endpoint == undefined) | ||
| 94 | throw new Error(`env var TINYAUTH_ENDPOINT not defined`); | ||
| 95 | return endpoint; | ||
| 96 | } | ||
| 97 | |||
| 98 | export function Auth_tinyauth_public_endpoint(): string { | ||
| 99 | const endpoint = process.env.TINYAUTH_PUBLIC_ENDPOINT; | ||
| 100 | if (endpoint == undefined) | ||
| 101 | throw new Error(`env var TINYAUTH_PUBLIC_ENDPOINT not defined`); | ||
| 102 | return endpoint; | ||
| 103 | } | ||
diff --git a/frontend/lib/drive.ts b/frontend/lib/drive.ts new file mode 100644 index 0000000..4211949 --- /dev/null +++ b/frontend/lib/drive.ts | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | import { spawnSync } from 'child_process' | ||
| 2 | |||
| 3 | export interface DriveLsEntry { | ||
| 4 | path: string | ||
| 5 | type: "dir" | "file" | ||
| 6 | lastmod: number | ||
| 7 | blob: string | null | ||
| 8 | size: number | null | ||
| 9 | author: string | ||
| 10 | } | ||
| 11 | |||
| 12 | /// lists the given path on the drive | ||
| 13 | export async function Drive_ls(path: string, recursive: boolean): Promise<DriveLsEntry[]> { | ||
| 14 | const args = ['ls'] | ||
| 15 | if (recursive) { | ||
| 16 | args.push('-r') | ||
| 17 | } | ||
| 18 | if (path) { | ||
| 19 | args.push(path) | ||
| 20 | } | ||
| 21 | |||
| 22 | const result = spawnSync('fctdrive', args, { encoding: 'utf-8' }) | ||
| 23 | if (result.error) { | ||
| 24 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 25 | } | ||
| 26 | if (result.status !== 0) { | ||
| 27 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 28 | } | ||
| 29 | const stdout = result.stdout | ||
| 30 | const entries = [] | ||
| 31 | for (const line of stdout.split('\n')) { | ||
| 32 | if (line.trim() == "") | ||
| 33 | continue; | ||
| 34 | |||
| 35 | const parts = line.split('\t'); | ||
| 36 | const path = parts[0]; | ||
| 37 | const type = parts[1]; | ||
| 38 | const lastmod = parseInt(parts[2]); | ||
| 39 | const blobStr = parts[3]; | ||
| 40 | const sizeStr = parts[4]; | ||
| 41 | const author = parts[5]; | ||
| 42 | |||
| 43 | var blob = null; | ||
| 44 | if (blobStr != "-") | ||
| 45 | blob = blobStr; | ||
| 46 | |||
| 47 | var size = null; | ||
| 48 | if (sizeStr != "-") | ||
| 49 | size = parseFloat(sizeStr); | ||
| 50 | |||
| 51 | entries.push({ | ||
| 52 | path, type, lastmod, blob, size, author | ||
| 53 | } as DriveLsEntry); | ||
| 54 | } | ||
| 55 | return entries; | ||
| 56 | } | ||
| 57 | |||
| 58 | /// import the file at local_path by moving it to drive_path | ||
| 59 | export async function Drive_import(local_path: string, drive_path: string, email: string) { | ||
| 60 | const result = spawnSync('fctdrive', ['import', local_path, "--mode", "move", "--destination", drive_path, "--email", email], { encoding: 'utf-8' }); | ||
| 61 | if (result.error) { | ||
| 62 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`); | ||
| 63 | } | ||
| 64 | if (result.status !== 0) { | ||
| 65 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | /// returns the full filesystem path of the blob given its id | ||
| 70 | export async function Drive_blob_path(blob: string): Promise<string> { | ||
| 71 | const result = spawnSync('fctdrive', ['blob', blob], { encoding: 'utf-8' }) | ||
| 72 | if (result.error) { | ||
| 73 | throw new Error(`Failed to execute fctdrive: ${result.error.message}`) | ||
| 74 | } | ||
| 75 | if (result.status !== 0) { | ||
| 76 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`) | ||
| 77 | } | ||
| 78 | return result.stdout.trim(); | ||
| 79 | } | ||
| 80 | |||
| 81 | export function Drive_basename(path: string): string { | ||
| 82 | const parts = path.split('/'); | ||
| 83 | return parts[parts.length - 1]; | ||
| 84 | } | ||
| 85 | |||
| 86 | export function Drive_parent(path: string): string | null { | ||
| 87 | const parts = path.split('/'); | ||
| 88 | if (parts.length <= 1) | ||
| 89 | return null; | ||
| 90 | return parts[parts.length - 2]; | ||
| 91 | } | ||
diff --git a/frontend/lib/env.ts b/frontend/lib/env.ts new file mode 100644 index 0000000..d67fb3c --- /dev/null +++ b/frontend/lib/env.ts | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | export function Env_is_development(): boolean { | ||
| 2 | return process.env.NODE_ENV == "development"; | ||
| 3 | } | ||
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000..7a1e30c --- /dev/null +++ b/frontend/lib/utils.ts | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | /** | ||
| 2 | * Formats a size in bytes into a human-readable string | ||
| 3 | * @param bytes Size in bytes | ||
| 4 | * @returns Formatted size string (e.g., "1.5 KB", "2.3 MB", "1.2 GB") | ||
| 5 | */ | ||
| 6 | export function formatSize(bytes: number | null): string { | ||
| 7 | if (bytes === null || bytes === 0) { | ||
| 8 | return '-' | ||
| 9 | } | ||
| 10 | |||
| 11 | const units = ['B', 'KB', 'MB', 'GB', 'TB'] | ||
| 12 | let size = bytes | ||
| 13 | let unitIndex = 0 | ||
| 14 | |||
| 15 | while (size >= 1024 && unitIndex < units.length - 1) { | ||
| 16 | size /= 1024 | ||
| 17 | unitIndex++ | ||
| 18 | } | ||
| 19 | |||
| 20 | // Format with appropriate decimal places | ||
| 21 | if (size < 10 && unitIndex > 0) { | ||
| 22 | return `${size.toFixed(1)} ${units[unitIndex]}` | ||
| 23 | } else { | ||
| 24 | return `${Math.round(size)} ${units[unitIndex]}` | ||
| 25 | } | ||
| 26 | } \ No newline at end of file | ||
