mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 00:30:02 +00:00
WIP: Notifications (via email, more to come?)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080">
|
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
@@ -60,6 +60,9 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
||||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||||
|
|
||||||
// Development
|
// Development
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export default function ConfigFormField({configElement, ...props}: any) {
|
|||||||
);
|
);
|
||||||
case "String":
|
case "String":
|
||||||
return (
|
return (
|
||||||
<Input label={configElement.description} name={configElement.key} type="text" {...props}/>
|
<Input label={configElement.description} name={configElement.key}
|
||||||
|
type={props.type && "text"} {...props}/>
|
||||||
);
|
);
|
||||||
case "Float":
|
case "Float":
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from "react";
|
||||||
|
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||||
|
import Section from "Frontend/components/general/Section";
|
||||||
|
import {Button, Input, Select, SelectItem} from "@nextui-org/react";
|
||||||
|
import {NotificationEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
import EmailCredentialsDto from "Frontend/generated/de/grimsi/gameyfin/notifications/dto/EmailCredentialsDto";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
|
function NotificationManagementLayout({getConfig, formik}: any) {
|
||||||
|
|
||||||
|
async function testMailSettings() {
|
||||||
|
const credentials: EmailCredentialsDto = {
|
||||||
|
host: formik.values.notifications.email.host,
|
||||||
|
port: formik.values.notifications.email.port,
|
||||||
|
username: formik.values.notifications.email.username,
|
||||||
|
password: formik.values.notifications.email.password
|
||||||
|
}
|
||||||
|
|
||||||
|
const areCredentialsValid = await NotificationEndpoint.verifyEmailCredentials(credentials);
|
||||||
|
|
||||||
|
if (areCredentialsValid) {
|
||||||
|
toast.success("Credentials are valid")
|
||||||
|
} else {
|
||||||
|
toast.error("Credentials are invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<ConfigFormField configElement={getConfig("notifications.enabled")}/>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-8">
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Section title="E-Mail"/>
|
||||||
|
<ConfigFormField configElement={getConfig("notifications.email.host")}
|
||||||
|
isDisabled={!formik.values.notifications.enabled}/>
|
||||||
|
<ConfigFormField configElement={getConfig("notifications.email.port")}
|
||||||
|
isDisabled={!formik.values.notifications.enabled}/>
|
||||||
|
<ConfigFormField configElement={getConfig("notifications.email.username")}
|
||||||
|
isDisabled={!formik.values.notifications.enabled}/>
|
||||||
|
<ConfigFormField configElement={getConfig("notifications.email.password")}
|
||||||
|
type="password"
|
||||||
|
isDisabled={!formik.values.notifications.enabled}/>
|
||||||
|
<Button onPress={testMailSettings}
|
||||||
|
isDisabled={!(
|
||||||
|
formik.values.notifications.enabled &&
|
||||||
|
formik.values.notifications.email.host &&
|
||||||
|
formik.values.notifications.email.port &&
|
||||||
|
formik.values.notifications.email.username)}>Test</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Section title="Push"/>
|
||||||
|
{/* TODO: Evaluate need and options if need is given */}
|
||||||
|
<Select className="mt-2 mb-10"
|
||||||
|
label="Push notification provider" defaultSelectedKeys={["pushbullet"]}
|
||||||
|
isDisabled>
|
||||||
|
<SelectItem key="pushbullet">Pushbullet</SelectItem>
|
||||||
|
</Select>
|
||||||
|
<Input className="mt-2 mb-10" label="Access Token" type="password" isDisabled/>
|
||||||
|
<Input className="mt-2 mb-10" label="Channel tag" type="text" isDisabled/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({});
|
||||||
|
|
||||||
|
export const NotificationManagement = withConfigPage(NotificationManagementLayout, "Notifications", "notifications", validationSchema);
|
||||||
@@ -58,6 +58,7 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
|||||||
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
||||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.client-secret")}
|
<ConfigFormField configElement={getConfig("sso.oidc.client-secret")}
|
||||||
|
type="password"
|
||||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.issuer-url")}
|
<ConfigFormField configElement={getConfig("sso.oidc.issuer-url")}
|
||||||
@@ -65,7 +66,7 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
|||||||
<Button
|
<Button
|
||||||
isDisabled={isAutoPopulateDisabled()}
|
isDisabled={isAutoPopulateDisabled()}
|
||||||
onPress={autoPopulate}
|
onPress={autoPopulate}
|
||||||
className="h-14 mt-2"><MagicWand/> Auto-populate</Button>
|
className="h-14 mt-2"><MagicWand className="min-w-5"/> Auto-populate</Button>
|
||||||
</div>
|
</div>
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.authorize-url")}
|
<ConfigFormField configElement={getConfig("sso.oidc.authorize-url")}
|
||||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Input = ({label, ...props}) => {
|
|||||||
const [field, meta] = useField(props);
|
const [field, meta] = useField(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-grow items-start gap-2 my-2">
|
<div className="flex flex-col w-full items-start gap-2 my-2">
|
||||||
<NextUiInput
|
<NextUiInput
|
||||||
{...props}
|
{...props}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -17,7 +17,7 @@ const Input = ({label, ...props}) => {
|
|||||||
label={label}
|
label={label}
|
||||||
isInvalid={meta.touched && !!meta.error}
|
isInvalid={meta.touched && !!meta.error}
|
||||||
/>
|
/>
|
||||||
<div className="min-h-6 w-full text-danger">
|
<div className="min-h-6 text-danger">
|
||||||
{meta.touched && meta.error && (
|
{meta.touched && meta.error && (
|
||||||
<SmallInfoField icon={XCircle} message={meta.error}/>
|
<SmallInfoField icon={XCircle} message={meta.error}/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ProfileManagement from "Frontend/components/administration/ProfileManagem
|
|||||||
import {SsoManagement} from "Frontend/components/administration/SsoManagement";
|
import {SsoManagement} from "Frontend/components/administration/SsoManagement";
|
||||||
import {AdministrationView} from "Frontend/views/AdministrationView";
|
import {AdministrationView} from "Frontend/views/AdministrationView";
|
||||||
import {ProfileView} from "Frontend/views/ProfileView";
|
import {ProfileView} from "Frontend/views/ProfileView";
|
||||||
|
import {NotificationManagement} from "Frontend/components/administration/NotificationManagement";
|
||||||
|
|
||||||
export const routes = protectRoutes([
|
export const routes = protectRoutes([
|
||||||
{
|
{
|
||||||
@@ -40,6 +41,7 @@ export const routes = protectRoutes([
|
|||||||
{path: 'libraries', element: <LibraryManagement/>},
|
{path: 'libraries', element: <LibraryManagement/>},
|
||||||
{path: 'users', element: <UserManagement/>},
|
{path: 'users', element: <UserManagement/>},
|
||||||
{path: 'sso', element: <SsoManagement/>},
|
{path: 'sso', element: <SsoManagement/>},
|
||||||
|
{path: 'notifications', element: <NotificationManagement/>}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -127,17 +127,37 @@ sealed class ConfigProperties<T : Serializable>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** Notifications */
|
/** Notifications */
|
||||||
data object NotificationsEmailHost :
|
data object NotificationsEnabled : ConfigProperties<Boolean>(
|
||||||
ConfigProperties<String>(String::class, "notifications.email.host", "URL of the email server")
|
Boolean::class,
|
||||||
|
"notifications.enabled",
|
||||||
|
"Enable notifications",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
data object NotificationsEmailPort :
|
data object NotificationsEmailHost : ConfigProperties<String>(
|
||||||
ConfigProperties<String>(String::class, "notifications.email.port", "Port of the email server")
|
String::class,
|
||||||
|
"notifications.email.host",
|
||||||
|
"URL of the email server"
|
||||||
|
)
|
||||||
|
|
||||||
data object NotificationsEmailUsername :
|
data object NotificationsEmailPort : ConfigProperties<Int>(
|
||||||
ConfigProperties<String>(String::class, "notifications.email.username", "Username for the email account")
|
Int::class,
|
||||||
|
"notifications.email.port",
|
||||||
|
"Port of the email server",
|
||||||
|
587
|
||||||
|
)
|
||||||
|
|
||||||
data object NotificationsEmailPassword :
|
data object NotificationsEmailUsername : ConfigProperties<String>(
|
||||||
ConfigProperties<String>(String::class, "notifications.email.password", "Password for the email account")
|
String::class,
|
||||||
|
"notifications.email.username",
|
||||||
|
"Username for the email account"
|
||||||
|
)
|
||||||
|
|
||||||
|
data object NotificationsEmailPassword : ConfigProperties<String>(
|
||||||
|
String::class,
|
||||||
|
"notifications.email.password",
|
||||||
|
"Password for the email account"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MatchUsersBy {
|
enum class MatchUsersBy {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ConfigService(
|
|||||||
* @throws IllegalArgumentException if the value can't be cast to the type defined for the config property
|
* @throws IllegalArgumentException if the value can't be cast to the type defined for the config property
|
||||||
*/
|
*/
|
||||||
fun <T : Serializable> set(key: String, value: T) {
|
fun <T : Serializable> set(key: String, value: T) {
|
||||||
log.info { "Set config value '$key' to '$value'" }
|
log.info { "Set config value '$key'" }
|
||||||
|
|
||||||
val configKey = findConfigProperty(key)
|
val configKey = findConfigProperty(key)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.grimsi.gameyfin.notifications
|
||||||
|
|
||||||
|
import com.vaadin.hilla.Endpoint
|
||||||
|
import de.grimsi.gameyfin.meta.Roles
|
||||||
|
import de.grimsi.gameyfin.notifications.dto.EmailCredentialsDto
|
||||||
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
import jakarta.mail.MessagingException
|
||||||
|
import jakarta.mail.Session
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Endpoint
|
||||||
|
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
||||||
|
class NotificationEndpoint {
|
||||||
|
|
||||||
|
fun verifyEmailCredentials(credentials: EmailCredentialsDto): Boolean {
|
||||||
|
val properties = Properties()
|
||||||
|
properties["mail.smtp.auth"] = "true"
|
||||||
|
properties["mail.smtp.starttls.enable"] = "true"
|
||||||
|
properties["mail.smtp.host"] = credentials.host
|
||||||
|
properties["mail.smtp.port"] = credentials.port
|
||||||
|
|
||||||
|
val session = Session.getInstance(properties, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val transport = session.getTransport("smtp")
|
||||||
|
transport.connect(credentials.host, credentials.port, credentials.username, credentials.password)
|
||||||
|
transport.close()
|
||||||
|
return true
|
||||||
|
} catch (ex: MessagingException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.grimsi.gameyfin.notifications.dto
|
||||||
|
|
||||||
|
data class EmailCredentialsDto(
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val username: String,
|
||||||
|
val password: String?
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user