diff --git a/backend/api/routes/accounts_api.py b/backend/api/routes/accounts_api.py index ea013de06..148da73b0 100644 --- a/backend/api/routes/accounts_api.py +++ b/backend/api/routes/accounts_api.py @@ -1,5 +1,5 @@ from flask import request -from flask_jwt_extended import jwt_required +from flask_jwt_extended import jwt_required, current_user from flask_restx import Namespace, Resource from helpers.api import convert_to_form @@ -43,6 +43,13 @@ def post(self): return create_account(**request.form), 200 + @api.doc(description="Update account") + @api.response(200, "Successfully updated account") + @api.response(500, "Internal server error") + def put(self): + """Update account""" + return update_account(current_user['id'], **request.form), 200 + @api.route("/") @api.route("//", doc=False) diff --git a/backend/helpers/universal.py b/backend/helpers/universal.py index 832facd95..fb25bd858 100644 --- a/backend/helpers/universal.py +++ b/backend/helpers/universal.py @@ -252,6 +252,12 @@ def global_invite_user_to_media_server(**kwargs) -> dict[str]: # pylint: disable=no-value-for-parameter db_user.execute() + # Set the invite to used + invite.used = True + invite.used_at = datetime.now() + invite.used_by = user.id if server_type == "plex" else user["Id"] + invite.save() + # Emit done socketio_emit("done", None) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index bab261d62..c014272e3 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -8,10 +8,10 @@ import ToastPlugin from "vue-toastification"; import ToastOptions from "./assets/configs/DefaultToasts"; import i18n from "./i18n"; import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; -import sentry from "./sentry"; import VueProgressBar from "@aacassandra/vue3-progressbar"; import FloatingVue from "floating-vue"; import router from "./router"; +import Sentry from "./plugins/sentry"; import Toast, { piniaPluginToast } from "./plugins/toasts"; import Axios, { piniaPluginAxios } from "./plugins/axios"; @@ -41,6 +41,7 @@ app.use(FloatingVue); app.use(plugin, defaultConfig(formkitConfig)); app.use(Socket, { uri: window.location.origin }); app.use(Filters); +app.use(Sentry); pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginToast); @@ -48,8 +49,6 @@ pinia.use(piniaPluginAxios); pinia.use(piniaPluginSocketIO); pinia.use(piniaPluginFilters); -sentry(app, router); - app.mount("#app"); export { app, pinia }; diff --git a/frontend/src/plugins/sentry.ts b/frontend/src/plugins/sentry.ts new file mode 100644 index 000000000..5e41eca28 --- /dev/null +++ b/frontend/src/plugins/sentry.ts @@ -0,0 +1,44 @@ +import { BrowserTracing, Replay, init, vueRouterInstrumentation } from "@sentry/vue"; +import DefaultToast from "@/components/Toasts/DefaultToast.vue"; + +import type { Options, TracingOptions } from "@sentry/vue/types/types"; +import type { App } from "vue"; + +type SentryOptions = Partial< + Omit & { + tracingOptions: Partial; + } +>; + +const vuePluginSentry = { + install: (app: App, options?: SentryOptions) => { + init({ + dsn: "https://d1994be8f88578e14f1a4ac06ae65e89@o4505748808400896.ingest.sentry.io/4505780347666432", + integrations: [ + new BrowserTracing({ + tracePropagationTargets: [location.origin], + routingInstrumentation: vueRouterInstrumentation(app.config.globalProperties.$router), + }), + new Replay({ + maskAllText: false, + blockAllMedia: false, + maskAllInputs: false, + }), + ], + environment: process.env.NODE_ENV, + tracesSampleRate: 1.0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + beforeSend(event, hint) { + if (event.exception) { + const errorMessage = event.message ?? "Unknown error"; + app.config.globalProperties.$toast.error(DefaultToast("Detected Error", `${errorMessage}, this can generally be ignored.`)); + } + return event; + }, + ...options, + }); + }, +}; + +export default vuePluginSentry; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index a033c37ae..ca421d3bc 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -105,7 +105,13 @@ const router = createRouter({ path: "media", name: "admin-settings-media", component: () => import("@/views/SettingsViews/MediaView.vue"), - meta: { header: "Manage Media", subheader: "Configure server media" }, + meta: { header: "Manage Media", subheader: "Configure media server" }, + }, + { + path: "account", + name: "admin-settings-account", + component: () => import("@/views/SettingsViews/AccountView.vue"), + meta: { header: "Manage Account", subheader: "Configure your account" }, }, { path: "sessions", diff --git a/frontend/src/sentry.ts b/frontend/src/sentry.ts deleted file mode 100644 index 5d66494bf..000000000 --- a/frontend/src/sentry.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Sentry from "@sentry/vue"; -import { useServerStore } from "./stores/server"; -import type { App } from "vue"; -import type { Router } from "vue-router"; - -const init = (app: App, router: Router) => { - // Get the server store for debug statement - const serverStore = useServerStore(); - - // Initialize Sentry - Sentry.init({ - app, - dsn: "https://d1994be8f88578e14f1a4ac06ae65e89@o4505748808400896.ingest.sentry.io/4505780347666432", - integrations: [ - new Sentry.BrowserTracing({ - tracePropagationTargets: [location.origin], - routingInstrumentation: Sentry.vueRouterInstrumentation(router), - }), - new Sentry.Replay({ - maskAllText: false, - blockAllMedia: false, - maskAllInputs: false, - }), - ], - environment: process.env.NODE_ENV, - tracesSampleRate: 1.0, - replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, - }); -}; - -console.log(process.env.NODE_ENV); - -export default init; diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index 9c22ef8c2..a88ca4c7f 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -18,6 +18,21 @@ export const useUserStore = defineStore("user", { setUser(user: Partial) { this.user = user; }, + updateUser(user: Partial) { + // Create a new form data object + const formData = new FormData(); + if (user.display_name) formData.append("display_name", user.display_name); + if (user.username) formData.append("username", user.username); + if (user.email) formData.append("email", user.email); + + // Update the user in the database + this.$axios.put("/api/accounts", formData).then((response) => { + this.user = response.data; + }); + + // Update the user in the store + this.user = { ...this.user, ...user }; + }, }, persist: true, }); diff --git a/frontend/src/views/AdminViews/AdminView.vue b/frontend/src/views/AdminViews/AdminView.vue index e0e4cd9d5..933e0e833 100644 --- a/frontend/src/views/AdminViews/AdminView.vue +++ b/frontend/src/views/AdminViews/AdminView.vue @@ -3,10 +3,8 @@ -
- - - +
+
diff --git a/frontend/src/views/JoinViews/CarouselView.vue b/frontend/src/views/JoinViews/CarouselView.vue index e32e9f661..c523737ab 100644 --- a/frontend/src/views/JoinViews/CarouselView.vue +++ b/frontend/src/views/JoinViews/CarouselView.vue @@ -195,12 +195,6 @@ export default defineComponent({ if (response?.data?.room == undefined) { this.showError(this.__("Uh oh!"), this.__("Could not create the account.")); } - - // Socket step operation - this.socket?.on("step", (step: number) => { - this.eventBus.emit("step", step); - if (step == 3) setTimeout(() => this.currentView++, 1000); - }); }, async jellyfinCreateAccount(value: EmitterRecords["jellyfinCreateAccount"]) { // Show the next screen @@ -223,12 +217,6 @@ export default defineComponent({ if (response?.data?.room == undefined) { this.showError(this.__("Uh oh!"), this.__("Could not create the account.")); } - - // Socket step operation - this.socket?.on("step", (step: number) => { - this.eventBus.emit("step", step); - if (step == 3) setTimeout(() => this.currentView++, 1000); - }); }, }, async mounted() { @@ -239,6 +227,8 @@ export default defineComponent({ this.socket.on("error", (message) => this.showError(this.__("Uh oh!"), message)); this.socket.on("error", this.$toast.error); this.socket.on("message", this.$toast.info); + this.socket.on("step", (step: number) => this.eventBus.emit("step", step)); + this.socket.on("done", () => setTimeout(() => this.currentView++, 1000)); // Initialize the event bus this.eventBus.on("join", this.join); diff --git a/frontend/src/views/SettingsViews/AccountView.vue b/frontend/src/views/SettingsViews/AccountView.vue new file mode 100644 index 000000000..5dc887299 --- /dev/null +++ b/frontend/src/views/SettingsViews/AccountView.vue @@ -0,0 +1,53 @@ + + + diff --git a/frontend/src/views/SettingsViews/MediaView.vue b/frontend/src/views/SettingsViews/MediaView.vue index 85cbd194f..d890fa59c 100644 --- a/frontend/src/views/SettingsViews/MediaView.vue +++ b/frontend/src/views/SettingsViews/MediaView.vue @@ -143,6 +143,7 @@ export default defineComponent({ // Hide the scan servers button and show the test connection button this.buttonHidden.testConnection = false; + this.buttonHidden.saveServer = true; // Set the button to not loading this.buttonLoading.detectServer = false;