Compare commits
40 Commits
v1.0.0
...
internal-d
| Author | SHA1 | Date | |
|---|---|---|---|
| eb35a99c3e | |||
| 61233b3611 | |||
| 4ec441e688 | |||
| 263ec699ad | |||
| 6a1eef0f76 | |||
| ea27e4e8d5 | |||
| 50ba23fdb4 | |||
| e66c7dfb89 | |||
| c10f71b2db | |||
| 1ecab34d79 | |||
| 3d2bcc8ea6 | |||
| 4338f347c5 | |||
| 3eeaa797ad | |||
| 170d34c08c | |||
| 6edbf55756 | |||
| 1ac08fd656 | |||
| 3892c5ea4e | |||
| 4ed1fb9401 | |||
| 501580b2e0 | |||
| 048bcd1b5a | |||
| 2577dc46ee | |||
| c9189c4ef1 | |||
| be678c1cf0 | |||
| c020ea968c | |||
| 8486b95a45 | |||
| b3e14c1e02 | |||
| a9a6de8738 | |||
| 2c5c34eeac | |||
| 45448862c7 | |||
| 9670293b51 | |||
| 16e5d79460 | |||
| caf80f24ef | |||
| c0a1a0b944 | |||
| 5d39a150e7 | |||
| f7623483f8 | |||
| fb654afd11 | |||
| 3663061e02 | |||
| accf35e364 | |||
| 797a696aff | |||
| 8ecadcf2e5 |
@@ -9,7 +9,9 @@ RUN pip install --no-cache-dir -r requirements_prod.txt
|
||||
#RUN apt update && apt install ffmpeg -y
|
||||
|
||||
#COPY server/py/fastapi_server/ ./
|
||||
COPY ./server/py/fastapi_server/*.py ./
|
||||
COPY --chmod=755 ./server/py/fastapi_server/*.py ./
|
||||
# Copy the entrypoint to root
|
||||
COPY --chmod=755 ./server/py/fastapi_server/entrypoint.sh /
|
||||
|
||||
ENV PORT=8000
|
||||
ENV HOST=0.0.0.0
|
||||
@@ -18,6 +20,10 @@ ENV DOWNLOAD_FOLDER=/tmp/downloads
|
||||
# Ammount (in secs) before file autodeletes
|
||||
ENV FILE_AUTODELETE_DELAY_SECS=300
|
||||
|
||||
ENV ROOT_PATH=/
|
||||
|
||||
VOLUME ["/tmp/downloads"]
|
||||
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["python3", "main.py"]
|
||||
|
||||
@@ -23,9 +23,9 @@ ENV HOST=0.0.0.0
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV DOWNLOAD_FOLDER=/tmp/downloads
|
||||
ENV NUXT_DOWNLOAD_FOLDER=/tmp/downloads
|
||||
VOLUME ["/tmp/downloads"]
|
||||
|
||||
ENV FASTAPI_URL=http://127.0.0.1:8000
|
||||
ENV NUXT_FASTAPI_URL=http://127.0.0.1:8000
|
||||
|
||||
CMD ["node", "/app/server/index.mjs"]
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ui_colors_dropdown } from '~/utils/set_colors';
|
||||
|
||||
const loggedIn = ref(false);
|
||||
const logout_prompt = ref(false);
|
||||
@@ -27,14 +26,11 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
function logout() {
|
||||
return fetch("/logout").then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
)
|
||||
return $fetch("/logout").then((r) => {
|
||||
reloadNuxtApp({ path: useBasedPath("/") })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
6
app/composables/useBasedPath.ts
Normal file
6
app/composables/useBasedPath.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import urlJoin from "url-join"
|
||||
|
||||
export default function (route: string) {
|
||||
const base_url = useRuntimeConfig().app.baseURL
|
||||
return urlJoin(base_url, route)
|
||||
}
|
||||
7
app/layouts/default.vue
Normal file
7
app/layouts/default.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<slot />
|
||||
<UToaster />
|
||||
</template>
|
||||
@@ -20,4 +20,5 @@
|
||||
<MainHeader class="mb-0" :settings_link="false" :logged_in="loggedIn"/>
|
||||
<UNavigationMenu :items="settings_submenu" class="justify-center mt-2 mb-2"/>
|
||||
<slot />
|
||||
<UToaster />
|
||||
</template>
|
||||
@@ -1,12 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import urlJoin from 'url-join';
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const { loggedIn } = useUserSession()
|
||||
useHead({
|
||||
title: "Youtube 2 MP3"
|
||||
})
|
||||
definePageMeta({
|
||||
prerender: true // HTML prerender (faster)
|
||||
})
|
||||
|
||||
const { loggedIn } = useUserSession()
|
||||
const conf = useRuntimeConfig()
|
||||
|
||||
|
||||
const toast = useToast()
|
||||
const video = ref(0);
|
||||
const url = ref("");
|
||||
const in_url = ref("");
|
||||
const dwn_link = ref("");
|
||||
|
||||
const file_dwnl = ref("");
|
||||
|
||||
@@ -34,35 +46,39 @@
|
||||
|
||||
|
||||
async function start_conversion(event: FormSubmitEvent<musicParamsSchema>) {
|
||||
return fetch("/convert", {
|
||||
return $fetch("/convert", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
url: url.value,
|
||||
song: music_params.song,
|
||||
artist: music_params.artist,
|
||||
album: music_params.album
|
||||
})
|
||||
}).then(async r => {
|
||||
//console.log(r.status)
|
||||
if(r.status == 401)
|
||||
window.location.href = "/login"
|
||||
if(r.ok) {
|
||||
const data = await r.json(); // resolves to { file: "my_audio.mp3" }
|
||||
const file = data.file;
|
||||
file_dwnl.value = `/download/${file}`;
|
||||
const a = document.createElement("a");
|
||||
a.href = file_dwnl.value;
|
||||
a.download = file; // sets suggested filename
|
||||
document.body.appendChild(a); // must append first
|
||||
a.click(); // triggers download
|
||||
a.remove();
|
||||
}
|
||||
})
|
||||
}).then(r => { // r resolves to { file: "my_audio.mp3" }
|
||||
const file = r.file; // in this case r["file"] --> "my_audio.mp3"
|
||||
const a = document.createElement("a");
|
||||
a.href = useBasedPath(`/download/${file}`)
|
||||
a.download = file; // sets suggested filename
|
||||
file_dwnl.value = a.href; // this ref activates the Download button
|
||||
document.body.appendChild(a); // must append first
|
||||
a.click(); // triggers download
|
||||
a.remove();
|
||||
|
||||
}).catch(r => {
|
||||
// response is NOT ok (401, 500, etc...)
|
||||
const res = r.data;
|
||||
console.log(r.data)
|
||||
if(res.statusCode == 401) {
|
||||
navigateTo(useBasedPath("/login"),{external: true})
|
||||
}
|
||||
else toast.add({
|
||||
title: "Error",
|
||||
description: "Returned error. Check your YT link, if this happens too often, try again in a couple of minutes",
|
||||
color: "error"
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -75,10 +91,10 @@
|
||||
<UInput
|
||||
class="self-center geist-mono mt-5 mb-5 max-sm:w-[92%] max-md:w-4/5 max-lg:w-4/5 lg:w-2/3 max-w-300"
|
||||
:ui="{ base: 'rounded-2xl h-15 text-toned', trailing: 'p-0' }"
|
||||
placeholder="Search for a YouTube video..."
|
||||
placeholder="Paste a YouTube URL here..."
|
||||
v-model="in_url" >
|
||||
<template #trailing>
|
||||
<UButton class="self-end rounded-r-2xl h-15 px-3 hover:cursor-pointer" icon="lucide:search" @click="get_yt_vid" />
|
||||
<UButton class="self-end rounded-r-2xl h-15 px-3 hover:cursor-pointer" icon="lucide:arrow-right" @click="get_yt_vid" />
|
||||
</template>
|
||||
</UInput>
|
||||
|
||||
@@ -103,7 +119,7 @@
|
||||
<div class="text-center">
|
||||
<UButton class="mt-4 rounded-2xl px-9 w-2/3 py-3 justify-center text-center " loading-auto type="submit">Convert to MP3</UButton>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-center" v-if="dwn_link != ''">
|
||||
<UButton class="mt-4 rounded-2xl px-9 w-2/3 py-3 justify-center text-center" icon="lucide:cloud-download" color="success" >Download</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'
|
||||
import urlJoin from 'url-join'
|
||||
|
||||
useHead({
|
||||
title: "Login"
|
||||
})
|
||||
definePageMeta({
|
||||
ssr: false // CSR
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// animation & status refs
|
||||
const wrong_login = ref(false)
|
||||
const login_pending = ref(false)
|
||||
|
||||
// variable of timeout type
|
||||
let wrong_login_timeout : ReturnType<typeof setTimeout>
|
||||
|
||||
const fields: AuthFormField[] = [{
|
||||
name: 'email',
|
||||
@@ -29,28 +43,33 @@ const fields: AuthFormField[] = [{
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string('Password is required').min(8, 'Must be at least 8 characters')
|
||||
password: z.string('Password is required').min(8, 'Must be at least 8 characters'),
|
||||
remember: z.boolean().optional()
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
return fetch("/login", {
|
||||
async function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
clearTimeout(wrong_login_timeout); // cancel the timeout
|
||||
wrong_login.value = false // hide it if it was
|
||||
login_pending.value = true // mark it as pending
|
||||
|
||||
await $fetch("/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type" : "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
email: payload.data.email,
|
||||
password: payload.data.password
|
||||
})
|
||||
}).then((response) => {
|
||||
if (response.ok)
|
||||
window.location.href = "/";
|
||||
else if (response.status == 401) {
|
||||
password: payload.data.password,
|
||||
remember: payload.data.remember || false
|
||||
}
|
||||
}).then((r) => {
|
||||
login_pending.value = false
|
||||
navigateTo(useBasedPath("/"), { external: true})
|
||||
}).catch((err) => {
|
||||
if(err.data.statusCode == 401) {
|
||||
login_pending.value = false
|
||||
wrong_login.value = true;
|
||||
// show for 5 seconds
|
||||
setTimeout(() => wrong_login.value = false, 5000);
|
||||
wrong_login_timeout = setTimeout(() => wrong_login.value = false, 5000);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -68,7 +87,7 @@ function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
label: 'Login',
|
||||
class: 'rounded-full',
|
||||
variant: 'soft',
|
||||
loadingAuto: true
|
||||
loading: login_pending
|
||||
}"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: "settings",
|
||||
middleware: ["login-required"]
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
useHead({
|
||||
title: "Settings"
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
layout: "settings",
|
||||
ssr: false, // force CSR
|
||||
})
|
||||
const toast = useToast()
|
||||
const conf = useRuntimeConfig()
|
||||
const base_url = conf.app.baseURL.endsWith('/') ? conf.app.baseURL.slice(0, -1) : conf.app.baseURL
|
||||
|
||||
const { data: user_data, refresh: userRefresh } = useFetch(base_url + "/me");
|
||||
const loading_btn = ref(false)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
first_name: z.string(),
|
||||
last_name: z.string(),
|
||||
email: z.email()
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
async function submitChanges(event: FormSubmitEvent<Schema>) {
|
||||
loading_btn.value = true
|
||||
await fetch(useBasedPath("/settings/edit_user"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: user_data.value.first_name,
|
||||
last_name: user_data.value.last_name,
|
||||
email: user_data.value.email
|
||||
})
|
||||
}).then(async response => {
|
||||
|
||||
if (response.ok) {
|
||||
toast.add({
|
||||
title: "Done!",
|
||||
description: "It's changed!",
|
||||
color: "success"
|
||||
})
|
||||
}
|
||||
else {
|
||||
const body = await response.json()
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description : body.data.detail || 'Something went wrong',
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
loading_btn.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer class="max-w-4xl">
|
||||
<UForm :schema="schema" :state="user_data" @submit="submitChanges">
|
||||
<div class="gap-3 justify-between">
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="First Name" orientation="horizontal" name="first_name">
|
||||
<UInput v-if="user_data" v-model="user_data.first_name" required/>
|
||||
<USkeleton v-else class="w-53 h-7" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="Last Name" orientation="horizontal">
|
||||
<UInput v-if="user_data" v-model="user_data.last_name" required/>
|
||||
<USkeleton v-else class="w-53 h-7" />
|
||||
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="E-mail" orientation="horizontal" name="email">
|
||||
<UInput v-if="user_data" v-model="user_data.email" required/>
|
||||
<USkeleton v-else class="w-53 h-7" />
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="text-center justify-items-center">
|
||||
<UButton variant="soft" icon="lucide:circle-check-big" :loading="loading_btn" type="submit" size="lg" :block="true" class="max-w-4/5 mt-4 mb-4 rounded-full">Submit</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UForm>
|
||||
</UContainer>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ["login-required"]
|
||||
})
|
||||
navigateTo("/settings/general");
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import urlJoin from 'url-join'
|
||||
|
||||
useHead({
|
||||
title: "Security"
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
layout: "settings",
|
||||
middleware: ["login-required"]
|
||||
ssr: false // force CSR
|
||||
})
|
||||
const loading_btn = ref(false)
|
||||
const toast = useToast()
|
||||
const config = useRuntimeConfig()
|
||||
//const base_url = config.app.baseURL.endsWith('/') ? config.app.baseURL.slice(0, -1) : config.app.baseURL
|
||||
|
||||
|
||||
const schema = z.object({
|
||||
old_pw: z.string(),
|
||||
new_pw: z.string().min(8, "Min 8 chars"),
|
||||
confirm_new_pw: z.string().min(8, "Min 8 chars"),
|
||||
})
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const pw_fields = ref({
|
||||
old_pw: "",
|
||||
new_pw: "",
|
||||
confirm_new_pw: ""
|
||||
})
|
||||
|
||||
const show_pw = reactive({
|
||||
old_pw: false,
|
||||
new_pw: false,
|
||||
confirm_new_pw: false
|
||||
})
|
||||
|
||||
async function submitChanges(event: FormSubmitEvent<Schema>) {
|
||||
loading_btn.value = true
|
||||
try {
|
||||
fetch(useBasedPath("/settings/changepw"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(pw_fields.value)
|
||||
}).then(async (response) => {
|
||||
|
||||
if (response.ok) {
|
||||
toast.add({
|
||||
title: "Done!",
|
||||
description: "It's changed!",
|
||||
color: "success"
|
||||
})
|
||||
}
|
||||
else {
|
||||
const body = await response.json()
|
||||
console.log(body.statusMessage || "")
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description : body.data?.detail || body.statusMessage || 'Something went wrong',
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
catch(err:any) {
|
||||
toast.add({
|
||||
title: 'Error',
|
||||
description: err?.data?.statusMessage || err.message || 'Something went wrong',
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
loading_btn.value = false
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer class="max-w-4xl">
|
||||
<h1 class="text-lg ml-1 font-bold geist mb-1">Change password</h1>
|
||||
<USeparator />
|
||||
<div class="gap-3 justify-between">
|
||||
<UForm :schema="schema" :state="pw_fields" @submit="submitChanges">
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="Current password" orientation="horizontal" name="old_pw">
|
||||
<UInput :type="show_pw.old_pw ? 'text' : 'password'" v-model="pw_fields.old_pw" required>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
size="sm"
|
||||
:icon="show_pw.old_pw ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||
:aria-label="show_pw.old_pw ? 'Hide password' : 'Show password'"
|
||||
:aria-pressed="show_pw.old_pw"
|
||||
aria-controls="password"
|
||||
@click="show_pw.old_pw = !show_pw.old_pw"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="New password" orientation="horizontal" name="new_pw">
|
||||
<UInput :type="show_pw.new_pw ? 'text' : 'password'" v-model="pw_fields.new_pw" required>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
size="sm"
|
||||
:icon="show_pw.new_pw ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||
:aria-label="show_pw.new_pw ? 'Hide password' : 'Show password'"
|
||||
:aria-pressed="show_pw.new_pw"
|
||||
aria-controls="password"
|
||||
@click="show_pw.new_pw = !show_pw.new_pw"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="flex-1 w-full gap-2 mt-5 mb-5">
|
||||
<UFormField label="Confirm new password" orientation="horizontal" name="confirm_new_pw">
|
||||
<UInput :type="show_pw.confirm_new_pw ? 'text' : 'password'" v-model="pw_fields.confirm_new_pw" required>
|
||||
<template #trailing>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
size="sm"
|
||||
:icon="show_pw.confirm_new_pw ? 'i-lucide-eye-off' : 'i-lucide-eye'"
|
||||
:aria-label="show_pw.confirm_new_pw ? 'Hide password' : 'Show password'"
|
||||
:aria-pressed="show_pw.confirm_new_pw"
|
||||
aria-controls="password"
|
||||
@click="show_pw.confirm_new_pw = !show_pw.confirm_new_pw"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
</div>
|
||||
<div class="text-center justify-items-center">
|
||||
<UButton variant="soft" icon="lucide:circle-check-big" :loading="loading_btn" type="submit" size="lg" :block="true" class="max-w-4/5 mt-4 mb-4 rounded-full">Submit</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</div>
|
||||
</UContainer>
|
||||
|
||||
</template>
|
||||
@@ -20,8 +20,14 @@ services:
|
||||
#- bridge # allow external access
|
||||
volumes:
|
||||
- music_downloads:/tmp/downloads
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
- DB_MODE=sqlite
|
||||
- DB_MODE=mysql
|
||||
- MYSQL_HOST=host.docker.internal
|
||||
- MYSQL_USER=yt2mp3_nuxt
|
||||
- MYSQL_PASSWORD=blowfish
|
||||
- MYSQL_DB=yt2mp3_nuxt
|
||||
|
||||
nuxt:
|
||||
build:
|
||||
@@ -31,12 +37,12 @@ services:
|
||||
networks:
|
||||
- api_nuxt
|
||||
ports:
|
||||
- "64002:3030" # just for the admin GUI
|
||||
- "6903:3000"
|
||||
environment:
|
||||
- FASTAPI_URL=http://fastapi:8000
|
||||
- DOWNLOAD_FOLDER=/tmp/downloads
|
||||
- NUXT_FASTAPI_URL=http://fastapi:8000
|
||||
- NUXT_DOWNLOAD_FOLDER=/tmp/downloads
|
||||
- NUXT_SESSION_PASSWORD=bc7d4da7d5f64d47aa75da92bed68cd8
|
||||
- NUXT_APP_BASE_URL=/
|
||||
volumes:
|
||||
- music_downloads:/tmp/downloads
|
||||
# These 3 are just for "nuxtjs-launcher"
|
||||
|
||||
@@ -16,7 +16,11 @@ export default defineNuxtConfig({
|
||||
plugins: [ tailwindcss() ]
|
||||
},
|
||||
runtimeConfig: {
|
||||
fastapi_url: process.env.FASTAPI_URL || "http://127.0.0.1:8003",
|
||||
download_path: process.env.DOWNLOAD_FOLDER || "/tmp/downloads"
|
||||
// overridable by NUXT_variable
|
||||
fastapi_url: "http://fastapi:8000",
|
||||
download_path: "/tmp/downloads"
|
||||
},
|
||||
colorMode: {
|
||||
disableTransition: false // Enable the animation
|
||||
}
|
||||
})
|
||||
@@ -22,6 +22,7 @@
|
||||
"nuxt-auth-utils": "0.5.27",
|
||||
"nuxt-lazytube": "0.2.5",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"url-join": "^5.0.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4",
|
||||
"zod": "^4.3.5"
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
url-join:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
vue:
|
||||
specifier: ^3.5.26
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
@@ -4271,6 +4274,10 @@ packages:
|
||||
uqr@0.1.2:
|
||||
resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
|
||||
|
||||
url-join@5.0.0:
|
||||
resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -9206,6 +9213,8 @@ snapshots:
|
||||
|
||||
uqr@0.1.2: {}
|
||||
|
||||
url-join@5.0.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vaul-vue@0.4.1(reka-ui@2.6.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)):
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
29
server/middleware/redirects.ts
Normal file
29
server/middleware/redirects.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import urlJoin from "url-join";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event)
|
||||
const conf = useRuntimeConfig(event)
|
||||
const base_url = conf.app.baseURL
|
||||
|
||||
// normalize and remove base URL
|
||||
function stripBaseUrl(fullPath: string) {
|
||||
const base = base_url.replace(/\/+$/, '') // remove trailing slash
|
||||
return fullPath.startsWith(base) ? fullPath.slice(base.length) || '/' : fullPath
|
||||
}
|
||||
|
||||
let path = stripBaseUrl(event.node.req.url!)
|
||||
|
||||
|
||||
if (path?.startsWith("/settings"))
|
||||
// @ts-ignore
|
||||
if (!session.secure?.user_id)
|
||||
return sendRedirect(event, urlJoin(base_url,"/login"), 302);
|
||||
|
||||
else if (path?.startsWith("/login"))
|
||||
// @ts-ignore
|
||||
if (session.secure?.user_id)
|
||||
return sendRedirect(event, urlJoin(base_url,"/"), 302);
|
||||
|
||||
|
||||
return;
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
return;
|
||||
|
||||
// for now. this middleware is to be on redirect
|
||||
})
|
||||
9
server/py/.vscode/launch.json
vendored
Normal file
9
server/py/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
]
|
||||
}
|
||||
3
server/py/.vscode/settings.json
vendored
Normal file
3
server/py/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
63
server/py/fastapi_server/api.py
Normal file
63
server/py/fastapi_server/api.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from fastapi import Request, Response, HTTPException
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from user_mgmt import *
|
||||
import db_setup as db
|
||||
|
||||
async def register_endpoint(req: Request):
|
||||
body = await req.json()
|
||||
try:
|
||||
create_user(body["first_name"], body["last_name"], body["email"], body["password"])
|
||||
except KeyError:
|
||||
raise HTTPException(400, "Missing args")
|
||||
|
||||
|
||||
async def login_endpoint(req: Request):
|
||||
"""
|
||||
The login endpoint
|
||||
|
||||
Takes email and password
|
||||
Returns 401 or 400 in error.
|
||||
Returns the user's full info if correct
|
||||
"""
|
||||
|
||||
body = await req.json()
|
||||
#print(body)
|
||||
try:
|
||||
user_data = await fetch_user_info(body["email"], "email")
|
||||
if (user_data is None):
|
||||
print("User doesn't exist")
|
||||
raise HTTPException(status_code=401, detail="User doesn't exist")
|
||||
else:
|
||||
pwd = user_data.get("password")
|
||||
if (bcrypt.checkpw(body["password"].encode(), user_data.get("password").encode())):
|
||||
return user_data
|
||||
else:
|
||||
print("Wrong password")
|
||||
raise HTTPException(status_code=401, detail="Wrong password")
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=400)
|
||||
|
||||
|
||||
|
||||
async def me_get_endpoint(req: Request):
|
||||
"""
|
||||
Retrieves user profile data.
|
||||
|
||||
Trusts Nuxt's backend to allow it or not.
|
||||
"""
|
||||
args = dict(req.query_params)
|
||||
me = None
|
||||
try:
|
||||
# Prefer id over e-mail
|
||||
if "id" in args.keys(): me = fetch_user_info(args["id"], "id")
|
||||
elif "email" in args.keys(): me = fetch_user_info(args["email"], "email")
|
||||
else: raise HTTPException(400, "Provide either email or user id")
|
||||
#print(args)
|
||||
if (me is None):
|
||||
raise HTTPException(404, "User doesn't exist")
|
||||
else: return me
|
||||
except KeyError:
|
||||
raise HTTPException(400, "Please provide email")
|
||||
36
server/py/fastapi_server/db_setup.py
Normal file
36
server/py/fastapi_server/db_setup.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import aiomysql, aiosqlite, os
|
||||
from fastapi import FastAPI
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
# Either "mysql" or "sqlite"
|
||||
mode = os.environ.get("DB_MODE", "sqlite")
|
||||
# For SQLite usage
|
||||
sqlite_file = os.environ.get("SQLITE_FILE","./database.db")
|
||||
|
||||
# For MySQL usage
|
||||
mysql_pool = None
|
||||
|
||||
mysql_host = os.environ.get("MYSQL_HOST","127.0.0.1")
|
||||
mysql_user = os.environ.get("MYSQL_USER","yt2mp3")
|
||||
mysql_db_name = os.environ.get("MYSQL_DB","yt2mp3")
|
||||
mysql_pwd = os.environ.get("MYSQL_PASSWORD","")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
if mode == "mysql":
|
||||
try:
|
||||
print("Going with mysql")
|
||||
global mysql_pool
|
||||
mysql_pool = await aiomysql.create_pool(host=mysql_host,user=mysql_user,password=mysql_pwd,db=mysql_db_name, minsize=1, maxsize=10)
|
||||
yield
|
||||
finally:
|
||||
mysql_pool.close()
|
||||
await mysql_pool.wait_closed()
|
||||
elif mode == "sqlite":
|
||||
try:
|
||||
print("Going with sqlite")
|
||||
yield
|
||||
finally: pass # no action required now
|
||||
else:
|
||||
raise EnvironmentError("DB_MODE must either be mysql or sqlite")
|
||||
6
server/py/fastapi_server/entrypoint.sh
Normal file
6
server/py/fastapi_server/entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# make sure it remains updated (don't crash if fails)
|
||||
/usr/local/bin/pip install -U --no-cache-dir "yt-dlp[default]" || true
|
||||
|
||||
exec "$@"
|
||||
@@ -1,220 +1,155 @@
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
#from fastapi import Body, Cookie, File, Form, Header, Path, Query
|
||||
from contextlib import asynccontextmanager
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from typing import Optional
|
||||
import uvicorn
|
||||
import aiomysql
|
||||
# src/main.py
|
||||
from fastapi import FastAPI, HTTPException, status, Request
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
import os
|
||||
import bcrypt
|
||||
import asyncio
|
||||
import aiosqlite
|
||||
import subprocess
|
||||
import logging
|
||||
import uvicorn
|
||||
import copy
|
||||
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.utils import DownloadError
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#from pytube import YouTube
|
||||
#from moviepy.audio.io import AudioFileClip
|
||||
import db_setup as db # Import the entire db_setup module
|
||||
from user_mgmt import *
|
||||
from media_mgmt import *
|
||||
|
||||
#import asyncio
|
||||
pathRoot = os.environ.get("ROOT_PATH","/")
|
||||
|
||||
# In this FastAPI example, we skip BaseModel verification cause NuxtJS does that for us
|
||||
# In other words, this backend only recieves requests from nuxt js backend
|
||||
# So we can trust it
|
||||
app = FastAPI(
|
||||
title="YT2MP3 API",
|
||||
description="Simple API to download YouTube videos as MP3.",
|
||||
version="1.0.0",
|
||||
license_info="MIT",
|
||||
root_path=pathRoot,
|
||||
lifespan=db.lifespan
|
||||
)
|
||||
|
||||
class db:
|
||||
# Global error handler
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request, exc):
|
||||
logger.exception(f"An unexpected error occurred: {exc}")
|
||||
return JSONResponse(content={"error": "Internal Server Error"}, status_code=500)
|
||||
|
||||
mode = os.environ.get("DB_MODE", "sqlite")
|
||||
# Health check endpoint
|
||||
@app.get("/health", status_code=status.HTTP_200_OK)
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
|
||||
# For SQLite usage
|
||||
|
||||
file = os.environ.get("SQLITE_FILE","./database.db")
|
||||
|
||||
# For MySQL usage
|
||||
pool = None
|
||||
|
||||
host = os.environ.get("MYSQL_HOST","127.0.0.1")
|
||||
user = os.environ.get("MYSQL_USER","yt2mp3")
|
||||
db_name = os.environ.get("MYSQL_DB","yt2mp3")
|
||||
pwd = os.environ.get("MYSQL_PASSWORD","")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
if db.mode == "mysql":
|
||||
db.pool = await aiomysql.create_pool(host=db.host,user=db.user,password=db.pwd,db=db.db_name, minsize=1, maxsize=10)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
db.pool.close()
|
||||
await db.pool.wait_closed()
|
||||
#
|
||||
# User-related endpoints
|
||||
#
|
||||
@app.get("/me")
|
||||
async def me_endpoint(req: Request):
|
||||
query = dict(req.query_params)
|
||||
print(query)
|
||||
if "id" not in query.keys(): raise HTTPException(400, "Give ID")
|
||||
|
||||
user = await fetch_user_info(query["id"],"id")
|
||||
if user is None: raise HTTPException(404, "User not found")
|
||||
else:
|
||||
try:
|
||||
yield
|
||||
finally: pass # no action required now
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
async def fetch_user_info(arg: str, type: str):
|
||||
if db.mode == "mysql":
|
||||
async with db.pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute("SELECT * FROM users WHERE %s = %s", (type, arg))
|
||||
return await cur.fetchone()
|
||||
elif db.mode == "sqlite":
|
||||
async with aiosqlite.connect(db.file) as conn:
|
||||
async with conn.execute("SELECT * FROM users WHERE ? = ?", (type, arg)) as cursor:
|
||||
return await cursor.fetchone()
|
||||
|
||||
def download_audio(video_url: str, output_folder: str) -> str:
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'outtmpl': os.path.join(output_folder, '%(title)s.%(ext)s'),
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': '192',
|
||||
}, {
|
||||
'key': 'FFmpegMetadata',
|
||||
'add_metadata': True,
|
||||
}],
|
||||
'quiet': True,
|
||||
'noplaylist': True
|
||||
}
|
||||
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(video_url, download=True)
|
||||
filename = ydl.prepare_filename(info)
|
||||
return os.path.splitext(filename)[0] + ".mp3" # path to final MP3
|
||||
|
||||
def set_metadata_and_rename(mp3_file: str, name: str, artist: str = None, album: str = None) -> str:
|
||||
new_file = os.path.abspath(os.path.join(os.path.dirname(mp3_file), f"{name}.mp3"))
|
||||
|
||||
cmd = ["ffmpeg", "-y", "-i", mp3_file]
|
||||
|
||||
# Only add metadata if provided
|
||||
cmd += ["-metadata", f"title={name}"]
|
||||
if artist:
|
||||
cmd += ["-metadata", f"artist={artist}"]
|
||||
if album:
|
||||
cmd += ["-metadata", f"album={album}"]
|
||||
|
||||
cmd += [new_file]
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
os.remove(mp3_file) # clean original
|
||||
return new_file
|
||||
|
||||
|
||||
# Configuration (customize these!)
|
||||
DOWNLOAD_FOLDER = os.environ.get("DOWNLOAD_FOLDER","downloads") # Where the converted files will be saved
|
||||
if not os.path.exists(DOWNLOAD_FOLDER):
|
||||
os.makedirs(DOWNLOAD_FOLDER)
|
||||
|
||||
# Throw error if the env is incorrect
|
||||
try:
|
||||
AUTODELETE_DELAY_SECS = int(os.environ.get("FILE_AUTODELETE_DELAY_SECS", 0))
|
||||
except ValueError:
|
||||
raise EnvironmentError("You must set AUTODELETE_DELAY_SECS to a valid number (int) of seconds! To disable auto delete (not recommended), set it to 0")
|
||||
|
||||
async def delay_delete_file(file:str):
|
||||
if (AUTODELETE_DELAY_SECS > 0):
|
||||
await asyncio.sleep(AUTODELETE_DELAY_SECS)
|
||||
if os.path.exists(file): os.unlink(file)
|
||||
|
||||
@app.post("/convert")
|
||||
async def create_conversion(req: Request):
|
||||
"""Converts a YouTube video to MP3."""
|
||||
body = await req.json()
|
||||
print(body)
|
||||
video_url = body.get("video_url")
|
||||
user_id = body.get("user_id")
|
||||
|
||||
if (video_url is None): raise HTTPException(400, "Please provide an URL on body")
|
||||
if (body.get("user_id") is None): raise HTTPException(401, "No User ID")
|
||||
|
||||
USER_FOLDER = os.path.join(DOWNLOAD_FOLDER, str(user_id))
|
||||
if not os.path.exists(USER_FOLDER): os.mkdir(USER_FOLDER)
|
||||
try:
|
||||
file = await asyncio.to_thread(download_audio, video_url, USER_FOLDER)
|
||||
except DownloadError as e:
|
||||
raise HTTPException(500, "Error while downloading " + e.msg)
|
||||
final_file = await asyncio.to_thread(set_metadata_and_rename, file, body.get("song"), body.get("artist"), body.get("album"))
|
||||
file_name = final_file.split("/")[-1]
|
||||
|
||||
print("File served: " + final_file)
|
||||
asyncio.create_task(delay_delete_file(final_file))
|
||||
|
||||
return { "file" : file_name }
|
||||
|
||||
|
||||
|
||||
@app.post("/register")
|
||||
async def create_user(req: Request):
|
||||
body = await req.json()
|
||||
|
||||
# Auto exceptions give more verbose to Nuxt.
|
||||
|
||||
body["password"] = bcrypt.hashpw(body["password"].encode(), bcrypt.gensalt()).decode()
|
||||
if db.mode == "mysql":
|
||||
async with db.pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (%s,%s,%s,%s)", (body.first_name, body.last_name, body.email, body.password))
|
||||
await conn.commit()
|
||||
elif db.mode == "sqlite":
|
||||
async with aiosqlite.connect(db.file) as conn:
|
||||
await conn.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (?, ?, ?, ?)", (body.first_name, body.last_name, body.email, body.password))
|
||||
await conn.commit()
|
||||
|
||||
return user
|
||||
|
||||
@app.post("/login")
|
||||
async def login_endpoint(req: Request):
|
||||
|
||||
body = await req.json()
|
||||
#print(body)
|
||||
needed_keys = ("email","password")
|
||||
for key in needed_keys:
|
||||
if body.get(key) is None:
|
||||
raise HTTPException(400, "Missing args")
|
||||
|
||||
login_obj = await login_user(body["email"], body["password"])
|
||||
if login_obj is None:
|
||||
raise HTTPException(401, "Incorrect login")
|
||||
else:
|
||||
return login_obj
|
||||
|
||||
@app.post("/register")
|
||||
async def register_endpoint(req: Request):
|
||||
body = await req.json()
|
||||
|
||||
for key in ("email","password", "first_name", "last_name"):
|
||||
if body.get(key) is None:
|
||||
raise HTTPException(400, "Missing args")
|
||||
|
||||
# check if user exists.
|
||||
if await fetch_user_info(body["email"],"email") is not None:
|
||||
raise HTTPException(400, "User already exists")
|
||||
|
||||
await create_user(body["first_name"], body["last_name"], body["email"], body["password"])
|
||||
return PlainTextResponse("User registered")
|
||||
|
||||
@app.post("/edit_user")
|
||||
async def edit_endpoint(req: Request):
|
||||
body:dict = await req.json()
|
||||
print(body)
|
||||
|
||||
if "user_id" not in body.keys(): raise HTTPException(400, "Provide user ID")
|
||||
|
||||
params = copy.deepcopy(body) # to create a completely new version (ridiculous but okay)
|
||||
params.pop("user_id")
|
||||
|
||||
await edit_user(body["user_id"], params)
|
||||
return PlainTextResponse("User edited")
|
||||
|
||||
@app.post("/changepw")
|
||||
async def changepw_endpoint(req: Request):
|
||||
body:dict = await req.json()
|
||||
print(body)
|
||||
|
||||
if "user_id" not in body.keys(): raise HTTPException(400, "Provide user ID")
|
||||
|
||||
params = copy.deepcopy(body) # to create a completely new version (ridiculous but okay)
|
||||
params.pop("user_id")
|
||||
params.pop("old_password")
|
||||
|
||||
user = await fetch_user_info(body["user_id"],"id")
|
||||
|
||||
if bcrypt.checkpw(body["old_password"].encode(), user["password"].encode()) == False:
|
||||
raise HTTPException(403, "Incorrect password")
|
||||
|
||||
|
||||
|
||||
await edit_user(body["user_id"], params)
|
||||
return PlainTextResponse("Password changed")
|
||||
#
|
||||
# Track-specific endpoints
|
||||
#
|
||||
@app.post("/convert")
|
||||
async def begin_conversion(req: Request):
|
||||
body:dict = await req.json()
|
||||
|
||||
video_url = body.get("video_url")
|
||||
user_id = body.get("user_id")
|
||||
quality = body.get("kbps", 256)
|
||||
|
||||
if (video_url is None): raise HTTPException(400, "Please provide an URL on body")
|
||||
if (user_id is None): raise HTTPException(401, "No User ID")
|
||||
|
||||
# User folder is DOWNLOAD_FOLDER/<userid>
|
||||
USER_FOLDER = os.path.join(DOWNLOAD_FOLDER, str(user_id))
|
||||
if not os.path.exists(USER_FOLDER): os.mkdir(USER_FOLDER)
|
||||
|
||||
try:
|
||||
user_data = await fetch_user_info(body["email"], "email")
|
||||
if (user_data is None):
|
||||
print("User doesn't exist")
|
||||
raise HTTPException(status_code=401, detail="User doesn't exist")
|
||||
else:
|
||||
pwd = user_data.get("password")
|
||||
if (bcrypt.checkpw(body["password"].encode(), user_data.get("password").encode())):
|
||||
return user_data
|
||||
else:
|
||||
print("Wrong password")
|
||||
raise HTTPException(status_code=401, detail="Wrong password")
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=400)
|
||||
#except ValidationError:
|
||||
# raise HTTPException(status_code=400)
|
||||
|
||||
|
||||
@app.get("/me")
|
||||
async def me_get_endpoint(req: Request):
|
||||
"""
|
||||
Retrieves user profile data.
|
||||
|
||||
Trusts Nuxt's backend to allow it or not.
|
||||
"""
|
||||
args = dict(req.query_params)
|
||||
me = None
|
||||
try:
|
||||
# Prefer id over e-mail
|
||||
if "id" in args.keys(): me = fetch_user_info(args["id"], "id")
|
||||
elif "email" in args.keys(): me = fetch_user_info(args["email"], "email")
|
||||
else: raise HTTPException(400, "Provide either email or user id")
|
||||
#print(args)
|
||||
if (me is None):
|
||||
raise HTTPException(404, "User doesn't exist")
|
||||
else: return me
|
||||
except KeyError:
|
||||
raise HTTPException(400, "Please provide email")
|
||||
|
||||
file = await asyncio.to_thread(download_audio, video_url, USER_FOLDER, quality)
|
||||
final_file = await asyncio.to_thread(set_metadata_and_rename, file, body.get("song"), body.get("artist"), body.get("album"))
|
||||
file_name = final_file.split("/")[-1]
|
||||
|
||||
print("File served: " + final_file)
|
||||
asyncio.create_task(delay_delete_file(final_file))
|
||||
|
||||
return { "file" : file_name }
|
||||
except KeyError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Missing parameter: {e}")
|
||||
except DownloadError as e:
|
||||
logger.exception(f"Download error: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to download video")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during media conversion: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to convert media")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
listen_host = str(os.environ.get("HOST", "127.0.0.1"))
|
||||
|
||||
234
server/py/fastapi_server/main.py.bak
Normal file
234
server/py/fastapi_server/main.py.bak
Normal file
@@ -0,0 +1,234 @@
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
#from fastapi import Body, Cookie, File, Form, Header, Path, Query
|
||||
from contextlib import asynccontextmanager
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from typing import Optional
|
||||
import uvicorn
|
||||
import aiomysql
|
||||
import os
|
||||
import bcrypt
|
||||
import asyncio
|
||||
import aiosqlite
|
||||
import subprocess
|
||||
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.utils import DownloadError
|
||||
|
||||
|
||||
|
||||
class db:
|
||||
|
||||
mode = os.environ.get("DB_MODE", "sqlite")
|
||||
|
||||
# For SQLite usage
|
||||
|
||||
file = os.environ.get("SQLITE_FILE","./database.db")
|
||||
|
||||
# For MySQL usage
|
||||
pool = None
|
||||
|
||||
host = os.environ.get("MYSQL_HOST","127.0.0.1")
|
||||
user = os.environ.get("MYSQL_USER","yt2mp3")
|
||||
db_name = os.environ.get("MYSQL_DB","yt2mp3")
|
||||
pwd = os.environ.get("MYSQL_PASSWORD","")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
if db.mode == "mysql":
|
||||
db.pool = await aiomysql.create_pool(host=db.host,user=db.user,password=db.pwd,db=db.db_name, minsize=1, maxsize=10)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
db.pool.close()
|
||||
await db.pool.wait_closed()
|
||||
else:
|
||||
try:
|
||||
yield
|
||||
finally: pass # no action required now
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
#
|
||||
# Fetch user info.
|
||||
# Returns a JSON of the user or None if nothing exists (like an incorrect email)
|
||||
#
|
||||
|
||||
async def fetch_user_info(arg: str, type: str):
|
||||
if db.mode == "mysql":
|
||||
async with db.pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute("SELECT * FROM users WHERE %s = %s", (type, arg))
|
||||
return await cur.fetchone()
|
||||
elif db.mode == "sqlite":
|
||||
async with aiosqlite.connect(db.file) as conn:
|
||||
async with conn.execute("SELECT * FROM users WHERE ? = ?", (type, arg)) as cursor:
|
||||
return await cursor.fetchone()
|
||||
|
||||
#
|
||||
# Downloads the video and extracts the audio
|
||||
#
|
||||
def download_audio(video_url: str, output_folder: str, kbps:int) -> str:
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'outtmpl': os.path.join(output_folder, '%(title)s.%(ext)s'),
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': str(kbps),
|
||||
}, {
|
||||
'key': 'FFmpegMetadata',
|
||||
'add_metadata': True,
|
||||
}],
|
||||
'quiet': True,
|
||||
'noplaylist': True
|
||||
}
|
||||
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(video_url, download=True)
|
||||
filename = ydl.prepare_filename(info)
|
||||
return os.path.splitext(filename)[0] + ".mp3" # path to final MP3
|
||||
|
||||
#
|
||||
# Uses FFmpeg to edit the artist and album if specified
|
||||
#
|
||||
def set_metadata_and_rename(mp3_file: str, name: str, artist: str = None, album: str = None) -> str:
|
||||
new_file = os.path.abspath(os.path.join(os.path.dirname(mp3_file), f"{name}.mp3"))
|
||||
|
||||
cmd = ["ffmpeg", "-y", "-i", mp3_file]
|
||||
|
||||
# Only add metadata if provided
|
||||
cmd += ["-metadata", f"title={name}"]
|
||||
if artist:
|
||||
cmd += ["-metadata", f"artist={artist}"]
|
||||
if album:
|
||||
cmd += ["-metadata", f"album={album}"]
|
||||
|
||||
cmd += [new_file]
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
os.remove(mp3_file) # clean original
|
||||
return new_file
|
||||
|
||||
|
||||
#
|
||||
# The download folder.
|
||||
# It goes:
|
||||
# FOLDER/<user_id>/<filename>
|
||||
#
|
||||
DOWNLOAD_FOLDER = os.environ.get("DOWNLOAD_FOLDER","/tmp/downloads") # Where the converted files will be saved
|
||||
if not os.path.exists(DOWNLOAD_FOLDER):
|
||||
os.makedirs(DOWNLOAD_FOLDER)
|
||||
|
||||
# Throw error if the env is incorrect
|
||||
try:
|
||||
AUTODELETE_DELAY_SECS = int(os.environ.get("FILE_AUTODELETE_DELAY_SECS", 0))
|
||||
except ValueError:
|
||||
raise EnvironmentError("You must set AUTODELETE_DELAY_SECS to a valid number (int) of seconds! To disable auto delete (not recommended), set it to 0")
|
||||
|
||||
#
|
||||
# Asyncioway of scheduling a file delete with asyncio.sleep() and os.unlink()
|
||||
#
|
||||
async def delay_delete_file(file:str):
|
||||
if (AUTODELETE_DELAY_SECS > 0):
|
||||
await asyncio.sleep(AUTODELETE_DELAY_SECS)
|
||||
if os.path.exists(file): os.unlink(file)
|
||||
|
||||
@app.post("/convert")
|
||||
async def create_conversion(req: Request):
|
||||
"""Converts a YouTube video to MP3."""
|
||||
body = await req.json()
|
||||
print(body)
|
||||
video_url = body.get("video_url")
|
||||
user_id = body.get("user_id")
|
||||
quality = body.get("kbps")
|
||||
|
||||
if (video_url is None): raise HTTPException(400, "Please provide an URL on body")
|
||||
if (body.get("user_id") is None): raise HTTPException(401, "No User ID")
|
||||
|
||||
# User folder is DOWNLOAD_FOLDER/<userid>
|
||||
USER_FOLDER = os.path.join(DOWNLOAD_FOLDER, str(user_id))
|
||||
if not os.path.exists(USER_FOLDER): os.mkdir(USER_FOLDER)
|
||||
|
||||
try:
|
||||
file = await asyncio.to_thread(download_audio, video_url, USER_FOLDER, quality)
|
||||
except DownloadError as e:
|
||||
raise HTTPException(500, "Error while downloading " + e.msg)
|
||||
final_file = await asyncio.to_thread(set_metadata_and_rename, file, body.get("song"), body.get("artist"), body.get("album"))
|
||||
file_name = final_file.split("/")[-1]
|
||||
|
||||
print("File served: " + final_file)
|
||||
asyncio.create_task(delay_delete_file(final_file))
|
||||
|
||||
return { "file" : file_name }
|
||||
|
||||
|
||||
|
||||
@app.post("/register")
|
||||
async def create_user(req: Request):
|
||||
body = await req.json()
|
||||
|
||||
|
||||
body["password"] = bcrypt.hashpw(body["password"].encode(), bcrypt.gensalt()).decode()
|
||||
if db.mode == "mysql":
|
||||
async with db.pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (%s,%s,%s,%s)", (body.first_name, body.last_name, body.email, body.password))
|
||||
await conn.commit()
|
||||
elif db.mode == "sqlite":
|
||||
async with aiosqlite.connect(db.file) as conn:
|
||||
await conn.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (?, ?, ?, ?)", (body.first_name, body.last_name, body.email, body.password))
|
||||
await conn.commit()
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
async def login_endpoint(req: Request):
|
||||
|
||||
body = await req.json()
|
||||
#print(body)
|
||||
try:
|
||||
user_data = await fetch_user_info(body["email"], "email")
|
||||
if (user_data is None):
|
||||
print("User doesn't exist")
|
||||
raise HTTPException(status_code=401, detail="User doesn't exist")
|
||||
else:
|
||||
pwd = user_data.get("password")
|
||||
if (bcrypt.checkpw(body["password"].encode(), user_data.get("password").encode())):
|
||||
return user_data
|
||||
else:
|
||||
print("Wrong password")
|
||||
raise HTTPException(status_code=401, detail="Wrong password")
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=400)
|
||||
#except ValidationError:
|
||||
# raise HTTPException(status_code=400)
|
||||
|
||||
|
||||
@app.get("/me")
|
||||
async def me_get_endpoint(req: Request):
|
||||
"""
|
||||
Retrieves user profile data.
|
||||
|
||||
Trusts Nuxt's backend to allow it or not.
|
||||
"""
|
||||
args = dict(req.query_params)
|
||||
me = None
|
||||
try:
|
||||
# Prefer id over e-mail
|
||||
if "id" in args.keys(): me = fetch_user_info(args["id"], "id")
|
||||
elif "email" in args.keys(): me = fetch_user_info(args["email"], "email")
|
||||
else: raise HTTPException(400, "Provide either email or user id")
|
||||
#print(args)
|
||||
if (me is None):
|
||||
raise HTTPException(404, "User doesn't exist")
|
||||
else: return me
|
||||
except KeyError:
|
||||
raise HTTPException(400, "Please provide email")
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
listen_host = str(os.environ.get("HOST", "127.0.0.1"))
|
||||
listen_port = int(os.environ.get("PORT", 3000))
|
||||
uvicorn.run(app,host=listen_host,port=listen_port)
|
||||
73
server/py/fastapi_server/media_mgmt.py
Normal file
73
server/py/fastapi_server/media_mgmt.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import subprocess, os, asyncio
|
||||
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.utils import DownloadError
|
||||
#
|
||||
# The download folder.
|
||||
# It goes:
|
||||
# FOLDER/<user_id>/<filename>
|
||||
#
|
||||
DOWNLOAD_FOLDER = os.environ.get("DOWNLOAD_FOLDER","/tmp/downloads") # Where the converted files will be saved
|
||||
if not os.path.exists(DOWNLOAD_FOLDER):
|
||||
os.makedirs(DOWNLOAD_FOLDER)
|
||||
|
||||
# Throw error if the env is incorrect
|
||||
try:
|
||||
AUTODELETE_DELAY_SECS = int(os.environ.get("FILE_AUTODELETE_DELAY_SECS", 0))
|
||||
except ValueError:
|
||||
raise EnvironmentError("You must set AUTODELETE_DELAY_SECS to a valid number (int) of seconds! To disable auto delete (not recommended), set it to 0")
|
||||
|
||||
#
|
||||
# Asyncioway of scheduling a file delete with asyncio.sleep() and os.unlink()
|
||||
#
|
||||
async def delay_delete_file(file:str):
|
||||
if (AUTODELETE_DELAY_SECS > 0):
|
||||
await asyncio.sleep(AUTODELETE_DELAY_SECS)
|
||||
if os.path.exists(file): os.unlink(file)
|
||||
|
||||
|
||||
#
|
||||
# Downloads the video and extracts the audio
|
||||
#
|
||||
def download_audio(video_url: str, output_folder: str, kbps:int) -> str:
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'outtmpl': os.path.join(output_folder, '%(title)s.%(ext)s'),
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': str(kbps),
|
||||
}, {
|
||||
'key': 'FFmpegMetadata',
|
||||
'add_metadata': True,
|
||||
}],
|
||||
'quiet': True,
|
||||
'noplaylist': True
|
||||
}
|
||||
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(video_url, download=True)
|
||||
filename = ydl.prepare_filename(info)
|
||||
return os.path.splitext(filename)[0] + ".mp3" # path to final MP3
|
||||
|
||||
#
|
||||
# Uses FFmpeg to edit the artist and album if specified
|
||||
#
|
||||
def set_metadata_and_rename(mp3_file: str, name: str, artist: str = None, album: str = None) -> str:
|
||||
new_file = os.path.abspath(os.path.join(os.path.dirname(mp3_file), f"{name}.mp3"))
|
||||
|
||||
cmd = ["ffmpeg", "-y", "-i", mp3_file]
|
||||
|
||||
# Only add metadata if provided
|
||||
cmd += ["-metadata", f"title={name}"]
|
||||
if artist:
|
||||
cmd += ["-metadata", f"artist={artist}"]
|
||||
if album:
|
||||
cmd += ["-metadata", f"album={album}"]
|
||||
|
||||
cmd += [new_file]
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
os.remove(mp3_file) # clean original
|
||||
return new_file
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
aiomysql==0.3.2
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
bcrypt==5.0.0
|
||||
click==8.3.1
|
||||
fastapi==0.128.0
|
||||
h11==0.16.0
|
||||
idna==3.11
|
||||
pydantic==2.12.5
|
||||
pydantic_core==2.41.5
|
||||
starlette==0.50.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
uvicorn==0.40.0
|
||||
|
||||
11
server/py/fastapi_server/requirements_prod.txt
Normal file → Executable file
11
server/py/fastapi_server/requirements_prod.txt
Normal file → Executable file
@@ -4,16 +4,25 @@ annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
bcrypt==5.0.0
|
||||
brotli==1.2.0
|
||||
certifi==2026.1.4
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
fastapi==0.128.0
|
||||
h11==0.16.0
|
||||
idna==3.11
|
||||
mutagen==1.47.0
|
||||
pycryptodomex==3.23.0
|
||||
pydantic==2.12.5
|
||||
pydantic_core==2.41.5
|
||||
PyMySQL==1.1.2
|
||||
requests==2.32.5
|
||||
starlette==0.50.0
|
||||
typing==3.7.4.3
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.40.0
|
||||
yt-dlp==2025.12.8
|
||||
websockets==16.0
|
||||
yt-dlp==2026.1.29
|
||||
yt-dlp-ejs==0.4.0
|
||||
|
||||
79
server/py/fastapi_server/user_mgmt.py
Normal file
79
server/py/fastapi_server/user_mgmt.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# fastapi_server/user_mgmt.py
|
||||
import db_setup as db
|
||||
import bcrypt
|
||||
import asyncio
|
||||
import aiomysql
|
||||
|
||||
import db_setup as db
|
||||
|
||||
async def fetch_user_info(arg: str, type: str):
|
||||
"""Fetches user info.
|
||||
This kind of query is supposed to either return ONE user or nothing.
|
||||
"""
|
||||
if db.mode == "mysql":
|
||||
async with db.mysql_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute(f"SELECT * FROM users WHERE {type} = %s", (arg,))
|
||||
#print(cur.description)
|
||||
return await cur.fetchone()
|
||||
elif db.mode == "sqlite":
|
||||
async with db.file.connect() as conn:
|
||||
async with conn.execute(f"SELECT * FROM users WHERE {type} = ?", (arg,)) as cursor:
|
||||
return await cursor.fetchone()
|
||||
return None
|
||||
|
||||
|
||||
async def create_user(first_name: str, last_name: str, email: str, password: str):
|
||||
"""Creates a new user."""
|
||||
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
||||
if db.mode == "mysql":
|
||||
async with db.mysql_pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (%s,%s,%s,%s)", (first_name, last_name, email, hashed_password.decode()))
|
||||
await conn.commit()
|
||||
return True
|
||||
elif db.mode == "sqlite":
|
||||
async with db.file.connect() as conn:
|
||||
await conn.execute("INSERT INTO users (`first_name`,`last_name`,`email`,`password`) VALUES (?, ?, ?, ?)", (first_name, last_name, email, hashed_password.decode()))
|
||||
await conn.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def login_user(email: str, password: str):
|
||||
"""Logs in a user."""
|
||||
user_data = await fetch_user_info(email, "email")
|
||||
print(user_data)
|
||||
if user_data is None:
|
||||
return None # User doesn't exist
|
||||
else:
|
||||
if bcrypt.checkpw(password.encode(), user_data.get("password").encode()):
|
||||
return user_data # Login successful
|
||||
else:
|
||||
return None # Incorrect password
|
||||
|
||||
|
||||
async def edit_user(user_id: int, updates: dict):
|
||||
"""Edits an existing user."""
|
||||
if db.mode == "mysql":
|
||||
placeholders = ", ".join(["%s"] * len(updates))
|
||||
if updates.get("password") is not None: # there is a password update
|
||||
updates["password"] = bcrypt.hashpw(updates["password"].encode(), bcrypt.gensalt()).decode()
|
||||
values = list(updates.values())
|
||||
query = f"UPDATE users SET {', '.join([f'{key} = %s' for key in updates.keys()])} WHERE id = %s"
|
||||
values.append(user_id)
|
||||
async with db.mysql_pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(query, values)
|
||||
await conn.commit()
|
||||
return True
|
||||
elif db.mode == "sqlite":
|
||||
placeholders = ", ".join(["?"] * len(updates))
|
||||
values = list(updates.values())
|
||||
values.append(user_id)
|
||||
query = f"UPDATE users SET {', '.join([f'{key} = ?' for key in updates.keys()])} WHERE id = ?"
|
||||
async with db.file.connect() as conn:
|
||||
await conn.execute(query, values)
|
||||
await conn.commit()
|
||||
return True
|
||||
return False
|
||||
@@ -9,9 +9,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const userId = session.secure?.user_id;
|
||||
if (userId == undefined || userId == null)
|
||||
throw createError({ status: 401 })
|
||||
throw createError({ statusCode: 401 })
|
||||
else if (body.song == undefined)
|
||||
throw createError({ status: 400, message: "Song name required"})
|
||||
throw createError({ statusCode: 400, statusMessage: "Song name required"})
|
||||
|
||||
const req = await fetch(config.fastapi_url + "/convert", {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const conf = useRuntimeConfig()
|
||||
|
||||
const body = await readBody(event)
|
||||
console.log(body)
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string('Password is required').min(8, 'Must be at least 8 characters'),
|
||||
remember: z.boolean().optional()
|
||||
})
|
||||
|
||||
let validated;
|
||||
try {
|
||||
validated = schema.parse(body); // throws if invalid
|
||||
} catch (err: any) {
|
||||
// Zod throws a ZodError
|
||||
return sendError(event, createError({
|
||||
statusCode: 400,
|
||||
statusMessage: err.errors?.[0]?.message || 'Invalid input'
|
||||
}));
|
||||
}
|
||||
|
||||
const is_https = await getRequestProtocol(event) == "https"
|
||||
//console.log(is_https)
|
||||
|
||||
if ( !body.email || !body.password )
|
||||
throw createError({ statusCode: 400, message: "Bad form. Use e-mail and password" })
|
||||
throw createError({ statusCode: 400, statusMessage: "Bad form. Use e-mail and password" })
|
||||
//console.log(body)
|
||||
const req = await fetch(conf.fastapi_url + "/login",
|
||||
{
|
||||
@@ -22,7 +42,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
)
|
||||
|
||||
if (!req.ok) throw createError({ statusCode: req.status })
|
||||
if (!req.ok) throw createError({ statusCode: req.status, statusMessage: "Error"})
|
||||
else {
|
||||
const body = await req.json();
|
||||
|
||||
@@ -35,7 +55,9 @@ export default defineEventHandler(async (event) => {
|
||||
secure: {
|
||||
user_id: body.id
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
maxAge: (body.rembember) ? 60 * 60 * 24 * 7 : undefined,
|
||||
cookie: {
|
||||
secure: is_https,
|
||||
sameSite: is_https ? 'none' : 'lax'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
// log them out
|
||||
console.log(await clearUserSession(event))
|
||||
await clearUserSession(event)
|
||||
|
||||
return;
|
||||
})
|
||||
23
server/routes/me.get.ts
Normal file
23
server/routes/me.get.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
const config = await useRuntimeConfig(event)
|
||||
//const body = await readBody(event)
|
||||
const session = await getUserSession(event)
|
||||
|
||||
// @ts-ignore
|
||||
const userId = session.secure?.user_id;
|
||||
if (userId == undefined || userId == null)
|
||||
throw createError({ statusCode: 401 })
|
||||
|
||||
const r = await fetch(config.fastapi_url + "/me" + '?' +
|
||||
new URLSearchParams({ id: userId }))
|
||||
|
||||
let body = await r.json();
|
||||
|
||||
try { delete body["id"]; } catch {}
|
||||
try { delete body["password"]; } catch {}
|
||||
try { delete body["created_at"] } catch {}
|
||||
try { delete body["updated_at"]; } catch {}
|
||||
|
||||
return body;
|
||||
})
|
||||
49
server/routes/settings/changepw.post.ts
Normal file
49
server/routes/settings/changepw.post.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
const body = await readBody(event)
|
||||
if (body.new_pw !== body.confirm_new_pw)
|
||||
throw createError({statusCode: 400, statusMessage: "Passwords don't match!"})
|
||||
|
||||
const session = await getUserSession(event)
|
||||
const config = await useRuntimeConfig(event)
|
||||
|
||||
const schema = z.object({
|
||||
old_pw: z.string(),
|
||||
new_pw: z.string().min(8, "Min 8 chars"),
|
||||
confirm_new_pw: z.string().min(8, "Min 8 chars"),
|
||||
})
|
||||
|
||||
let validated;
|
||||
try {
|
||||
validated = schema.parse(body); // throws if invalid
|
||||
} catch (err: any) {
|
||||
// Zod throws a ZodError
|
||||
return sendError(event, createError({
|
||||
statusCode: 400,
|
||||
statusMessage: err.errors?.[0]?.message || 'Invalid input'
|
||||
}));
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const userId = session.secure?.user_id;
|
||||
if (userId == undefined || userId == null)
|
||||
throw createError({ status: 401 })
|
||||
|
||||
const current_r = await fetch(config.fastapi_url + "/me" + '?' +
|
||||
new URLSearchParams({ id: userId }))
|
||||
if (!current_r.ok)
|
||||
if (current_r.status == 404)
|
||||
throw createError({statusCode: 404, statusMessage: "User doesn't exist"})
|
||||
|
||||
await $fetch(config.fastapi_url + "/changepw", {
|
||||
method: "POST",
|
||||
body: {
|
||||
user_id: userId,
|
||||
old_password: body.old_pw,
|
||||
password: body.new_pw
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
59
server/routes/settings/edit_user.post.ts
Normal file
59
server/routes/settings/edit_user.post.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = await useRuntimeConfig(event)
|
||||
let body = await readBody(event)
|
||||
console.log(body)
|
||||
const session = await getUserSession(event)
|
||||
|
||||
const schema = z.object({
|
||||
first_name: z.string(),
|
||||
last_name: z.string(),
|
||||
email: z.email()
|
||||
})
|
||||
let validated;
|
||||
try {
|
||||
validated = schema.parse(body); // throws if invalid
|
||||
} catch (err: any) {
|
||||
// Zod throws a ZodError
|
||||
return sendError(event, createError({
|
||||
statusCode: 400,
|
||||
statusMessage: err.errors?.[0]?.message || 'Invalid input'
|
||||
}));
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const userId = session.secure?.user_id;
|
||||
if (userId == undefined || userId == null)
|
||||
throw createError({ status: 401 })
|
||||
|
||||
const current_r = await fetch(config.fastapi_url + "/me" + '?' +
|
||||
new URLSearchParams({ id: userId }))
|
||||
|
||||
if (!current_r.ok)
|
||||
if (current_r.status == 404)
|
||||
throw createError({ statusCode: 404, statusMessage: "User not found"})
|
||||
|
||||
|
||||
const current_body = await current_r.json();
|
||||
try { delete current_body["id"]; } catch {}
|
||||
try { delete current_body["password"]; } catch {}
|
||||
try { delete current_body["created_at"] } catch {}
|
||||
try { delete current_body["updated_at"]; } catch {}
|
||||
|
||||
|
||||
//let differences: Record<string, any> = {};
|
||||
|
||||
let a = { ...body };
|
||||
a.user_id = userId!
|
||||
console.log(a)
|
||||
|
||||
//console.log("Differences: " + JSON.stringify(differences));
|
||||
|
||||
return await $fetch(config.fastapi_url + "/edit_user", {
|
||||
method: "POST",
|
||||
body: a,
|
||||
ignoreResponseError: true // don't throw exception
|
||||
})
|
||||
|
||||
})
|
||||
Reference in New Issue
Block a user