diff --git a/src/app.d.ts b/src/app.d.ts index 7bcd9d1..837385b 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -13,7 +13,7 @@ declare global { // General Interface Desing tips: // Use possibly undefined `?:` for when a property is optional, meaning it could be there, or it could be not applicable - // Use possibly null `| nulll` for when the property is expected to be there but could possbily be explicitly empty + // Use possibly null `| null` for when the property is expected to be there but could possbily be explicitly empty interface User { id: string diff --git a/src/lib/server/users.db b/src/lib/server/users.db index ef10ac1..df45a2b 100644 Binary files a/src/lib/server/users.db and b/src/lib/server/users.db differ diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index a031c36..53f8147 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -44,7 +44,7 @@ if (data.redirectLocation) formData.append('redirectLocation', data.redirectLocation) - return async ({ result }) => { + return ({ result }) => { if (result.type === 'failure') return ($newestAlert = ['warning', result.data?.message]) if (result.type === 'redirect') return goto(result.location) } diff --git a/src/routes/settings/connections/+page.server.ts b/src/routes/settings/connections/+page.server.ts index 7144db1..1df60d4 100644 --- a/src/routes/settings/connections/+page.server.ts +++ b/src/routes/settings/connections/+page.server.ts @@ -58,6 +58,7 @@ export const actions: Actions = { const tokenData: Jellyfin.JFTokens = { accessToken: authData.AccessToken, } + const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, { method: 'POST', headers: { apikey: SECRET_INTERNAL_API_KEY }, @@ -69,7 +70,7 @@ export const actions: Actions = { const newConnection: Jellyfin.JFConnection = await newConnectionResponse.json() return { newConnection } }, - youtubeMusicLogin: async ({ request }) => { + youtubeMusicLogin: async ({ request, fetch, locals }) => { const formData = await request.formData() const { code } = Object.fromEntries(formData) const client = new google.auth.OAuth2({ clientId: PUBLIC_YOUTUBE_API_CLIENT_ID, clientSecret: YOUTUBE_API_CLIENT_SECRET, redirectUri: 'http://localhost:5173' }) // DO NOT SHIP THIS. THE CLIENT SECRET SHOULD NOT BE MADE AVAILABLE TO USERS. MAKE A REQUEST TO THE LAZULI WEBSITE INSTEAD. @@ -92,6 +93,17 @@ export const actions: Actions = { username: userChannel.snippet?.title as string, profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined, } + + const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, { + method: 'POST', + headers: { apikey: SECRET_INTERNAL_API_KEY }, + body: JSON.stringify({ service: serviceData, tokens: tokenData }), + }) + + if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' }) + + const newConnection: YouTubeMusic.YTConnection = await newConnectionResponse.json() + return { newConnection } }, deleteConnection: async ({ request, fetch, locals }) => { const formData = await request.formData() diff --git a/src/routes/settings/connections/+page.svelte b/src/routes/settings/connections/+page.svelte index 31b0837..fb94def 100644 --- a/src/routes/settings/connections/+page.svelte +++ b/src/routes/settings/connections/+page.svelte @@ -13,78 +13,85 @@ export let data: PageServerData let connections = data.userConnections - const submitCredentials: SubmitFunction = async ({ formData, action, cancel }) => { - switch (action.search) { - case '?/authenticateJellyfin': - const { serverUrl, username, password } = Object.fromEntries(formData) + const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => { + const { serverUrl, username, password } = Object.fromEntries(formData) - if (!(serverUrl && username && password)) { - $newestAlert = ['caution', 'All fields must be filled out'] - return cancel() - } - try { - formData.set('serverUrl', new URL(serverUrl.toString()).origin) - } catch { - $newestAlert = ['caution', 'Server URL is invalid'] - return cancel() - } - - const deviceId = getDeviceUUID() - formData.append('deviceId', deviceId) - break - case '?/youtubeMusicLogin': - const code = await youtubeAuthenication() - formData.append('code', code) - break - case '?/deleteConnection': - break - default: - console.log(action.search) - cancel() + if (!(serverUrl && username && password)) { + $newestAlert = ['caution', 'All fields must be filled out'] + return cancel() + } + try { + formData.set('serverUrl', new URL(serverUrl.toString()).origin) + } catch { + $newestAlert = ['caution', 'Server URL is invalid'] + return cancel() } - return async ({ result }) => { + const deviceId = getDeviceUUID() + formData.append('deviceId', deviceId) + + return ({ result }) => { if (result.type === 'failure') { return ($newestAlert = ['warning', result.data?.message]) } else if (result.type === 'success') { - if (result.data?.newConnection) { - const newConnection: Connection = result.data.newConnection - connections = [newConnection, ...connections] + const newConnection: Connection = result.data!.newConnection + connections = [...connections, newConnection] - newConnectionModal = null - return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`]) - } else if (result.data?.deletedConnectionId) { - const id = result.data.deletedConnectionId - const indexToDelete = connections.findIndex((connection) => connection.id === id) - const serviceType = connections[indexToDelete].service.type + newConnectionModal = null + return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`]) + } + } + } - connections.splice(indexToDelete, 1) - connections = connections + const authenticateYouTube: SubmitFunction = async ({ formData, cancel }) => { + const googleLoginProcess = (): Promise => { + return new Promise((resolve) => { + // @ts-ignore (google variable is a global variable imported by html script tag) + const client = google.accounts.oauth2.initCodeClient({ + client_id: PUBLIC_YOUTUBE_API_CLIENT_ID, + scope: 'https://www.googleapis.com/auth/youtube', + ux_mode: 'popup', + callback: (response: any) => { + resolve(response.code) + }, + }) + client.requestCode() + }) + } - return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`]) - } - } else if (result.type === 'redirect') { - window.open(result.location, '_blank') + const code = await googleLoginProcess() + if (!code) cancel() + formData.append('code', code) + + return ({ result }) => { + if (result.type === 'failure') { + return ($newestAlert = ['warning', result.data?.message]) + } else if (result.type === 'success') { + const newConnection: Connection = result.data!.newConnection + connections = [...connections, newConnection] + return ($newestAlert = ['success', 'Added Youtube Music']) + } + } + } + + const deleteConnection: SubmitFunction = () => { + return ({ result }) => { + if (result.type === 'failure') { + return ($newestAlert = ['warning', result.data?.message]) + } else if (result.type === 'success') { + const id = result.data!.deletedConnectionId + const indexToDelete = connections.findIndex((connection) => connection.id === id) + const serviceType = connections[indexToDelete].service.type + + connections.splice(indexToDelete, 1) + connections = connections + + return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`]) } } } let newConnectionModal: ComponentType> | null = null - - const youtubeAuthenication = async (): Promise => { - return new Promise((resolve) => { - // @ts-ignore (google variable is a global variable imported by html script tag) - const client = google.accounts.oauth2.initCodeClient({ - client_id: PUBLIC_YOUTUBE_API_CLIENT_ID, - scope: 'https://www.googleapis.com/auth/youtube', - ux_mode: 'popup', - callback: (response: any) => { - resolve(response.code) - }, - }) - client.requestCode() - }) - }
@@ -94,7 +101,7 @@ -
+ @@ -103,11 +110,11 @@
{#each connections as connection} - + {/each}
{#if newConnectionModal !== null} - (newConnectionModal = null)} /> + (newConnectionModal = null)} /> {/if}