mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-23 17:52:36 -04:00
Added support for disabling password login and registration (#947)
Signed-off-by: Fred Heinecke <fred.heinecke@yahoo.com> Signed-off-by: solidDoWant <fred.heinecke@yahoo.com>
This commit is contained in:
parent
ce63da1448
commit
1f573a2553
@ -4,6 +4,13 @@
|
||||
# http route prefix (will listen to $KYOO_PREFIX/movie for example)
|
||||
KYOO_PREFIX=""
|
||||
|
||||
|
||||
# Optional authentication settings
|
||||
# Set to true to disable login with password (OIDC auth must be configured)
|
||||
# AUTHENTICATION_DISABLE_PASSWORD_LOGIN=true
|
||||
# Set to true to disable the creation of new users (OIDC auth must be configured)
|
||||
# AUTHENTICATION_DISABLE_USER_REGISTRATION=true
|
||||
|
||||
# Postgres settings
|
||||
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
|
||||
# The behavior of the below variables match what is documented here:
|
||||
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Authentication.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Disables the action if the specified environment variable is set to true.
|
||||
/// </summary>
|
||||
public class DisableOnEnvVarAttribute(string varName) : Attribute, IResourceFilter
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||
|
||||
if (config.GetValue(varName, false))
|
||||
context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
||||
}
|
@ -51,6 +51,16 @@ public class ServerInfo
|
||||
/// Check if kyoo's setup is finished.
|
||||
/// </summary>
|
||||
public SetupStep SetupStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if password login is enabled on this instance.
|
||||
/// </summary>
|
||||
public bool PasswordLoginEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if registration is enabled on this instance.
|
||||
/// </summary>
|
||||
public bool RegistrationEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class OidcInfo
|
||||
|
@ -26,6 +26,7 @@ using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Authentication.Attributes;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Authentication.Models.DTO;
|
||||
using Kyoo.Models;
|
||||
@ -206,6 +207,7 @@ public class AuthApi(
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
[DisableOnEnvVar("AUTHENTICATION_DISABLE_PASSWORD_LOGIN")]
|
||||
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
User? user = await users.GetOrDefault(
|
||||
@ -241,6 +243,7 @@ public class AuthApi(
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
||||
[DisableOnEnvVar("AUTHENTICATION_DISABLE_USER_REGISTRATION")]
|
||||
public async Task<ActionResult<JwtToken>> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
try
|
||||
|
@ -23,6 +23,7 @@ using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Core.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
|
||||
namespace Kyoo.Authentication.Views;
|
||||
@ -33,7 +34,8 @@ namespace Kyoo.Authentication.Views;
|
||||
[ApiController]
|
||||
[Route("info")]
|
||||
[ApiDefinition("Info", Group = UsersGroup)]
|
||||
public class InfoApi(PermissionOption options, MiscRepository info) : ControllerBase
|
||||
public class InfoApi(PermissionOption options, MiscRepository info, IConfiguration configuration)
|
||||
: ControllerBase
|
||||
{
|
||||
public async Task<ActionResult<ServerInfo>> GetInfo()
|
||||
{
|
||||
@ -51,6 +53,14 @@ public class InfoApi(PermissionOption options, MiscRepository info) : Controller
|
||||
))
|
||||
.ToDictionary(x => x.Key, x => x.Value),
|
||||
SetupStatus = await info.GetSetupStep(),
|
||||
PasswordLoginEnabled = !configuration.GetValue(
|
||||
"AUTHENTICATION_DISABLE_PASSWORD_LOGIN",
|
||||
false
|
||||
),
|
||||
RegistrationEnabled = !configuration.GetValue(
|
||||
"AUTHENTICATION_DISABLE_USER_REGISTRATION",
|
||||
false
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -61,6 +61,14 @@ export const ServerInfoP = z
|
||||
* Check if kyoo's setup is finished.
|
||||
*/
|
||||
setupStatus: z.nativeEnum(SetupStep),
|
||||
/*
|
||||
* True if password login is enabled on this instance.
|
||||
*/
|
||||
passwordLoginEnabled: z.boolean(),
|
||||
/*
|
||||
* True if registration is enabled on this instance.
|
||||
*/
|
||||
registrationEnabled: z.boolean(),
|
||||
})
|
||||
.transform((x) => {
|
||||
const baseUrl = Platform.OS === "web" ? x.publicUrl : "kyoo://";
|
||||
|
@ -18,11 +18,10 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type QueryPage, login } from "@kyoo/models";
|
||||
import { type QueryPage, ServerInfoP, login, useFetch } from "@kyoo/models";
|
||||
import { A, Button, H1, Input, P, ts } from "@kyoo/primitives";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { useRouter } from "solito/router";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
@ -35,6 +34,11 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
||||
apiUrl,
|
||||
error: initialError,
|
||||
}) => {
|
||||
const { data } = useFetch({
|
||||
path: ["info"],
|
||||
parser: ServerInfoP,
|
||||
});
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | undefined>(initialError);
|
||||
@ -53,43 +57,47 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl}>
|
||||
<H1>{t("login.login")}</H1>
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
variant="big"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
onPress={async () => {
|
||||
const { error } = await login("login", {
|
||||
username,
|
||||
password,
|
||||
apiUrl,
|
||||
});
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/", undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
/>
|
||||
<OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} />
|
||||
{data?.passwordLoginEnabled && (
|
||||
<>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
variant="big"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
onPress={async () => {
|
||||
const { error } = await login("login", {
|
||||
username,
|
||||
password,
|
||||
apiUrl,
|
||||
});
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/", undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<P>
|
||||
<Trans i18nKey="login.or-register">
|
||||
Don’t have an account? <A href={{ pathname: "/register", query: { apiUrl } }}>Register</A>
|
||||
|
@ -34,7 +34,7 @@ import { useRouter } from "solito/router";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ErrorView } from "../errors";
|
||||
|
||||
export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => {
|
||||
export const OidcLogin = ({ apiUrl, hideOr }: { apiUrl?: string; hideOr?: boolean }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useFetch({ options: { apiUrl }, ...OidcLogin.query() });
|
||||
@ -76,6 +76,7 @@ export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => {
|
||||
flexDirection: "row",
|
||||
width: percent(100),
|
||||
alignItems: "center",
|
||||
display: hideOr ? "none" : undefined,
|
||||
})}
|
||||
>
|
||||
<HR {...css({ flexGrow: 1 })} />
|
||||
|
@ -18,11 +18,10 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type QueryPage, login } from "@kyoo/models";
|
||||
import { type QueryPage, ServerInfoP, login, useFetch } from "@kyoo/models";
|
||||
import { A, Button, H1, Input, P, ts } from "@kyoo/primitives";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { useRouter } from "solito/router";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
@ -32,6 +31,11 @@ import { OidcLogin } from "./oidc";
|
||||
import { PasswordInput } from "./password-input";
|
||||
|
||||
export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
||||
const { data } = useFetch({
|
||||
path: ["info"],
|
||||
parser: ServerInfoP,
|
||||
});
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@ -52,50 +56,58 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl}>
|
||||
<H1>{t("login.register")}</H1>
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
|
||||
<OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} />
|
||||
{data?.registrationEnabled && (
|
||||
<>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
variant="big"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
/>
|
||||
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.email")}</P>
|
||||
<Input autoComplete="email" variant="big" onChangeText={(value) => setEmail(value)} />
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.email")}</P>
|
||||
<Input autoComplete="email" variant="big" onChangeText={(value) => setEmail(value)} />
|
||||
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
variant="big"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
variant="big"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
|
||||
{password !== confirm && (
|
||||
<P {...css({ color: (theme) => theme.colors.red })}>{t("login.password-no-match")}</P>
|
||||
{password !== confirm && (
|
||||
<P {...css({ color: (theme) => theme.colors.red })}>{t("login.password-no-match")}</P>
|
||||
)}
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
<Button
|
||||
text={t("login.register")}
|
||||
disabled={password !== confirm}
|
||||
onPress={async () => {
|
||||
const { error } = await login("register", { email, username, password, apiUrl });
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/", undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
<Button
|
||||
text={t("login.register")}
|
||||
disabled={password !== confirm}
|
||||
onPress={async () => {
|
||||
const { error } = await login("register", { email, username, password, apiUrl });
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/", undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}}
|
||||
{...css({
|
||||
m: ts(1),
|
||||
width: px(250),
|
||||
maxWidth: percent(100),
|
||||
alignSelf: "center",
|
||||
mY: ts(3),
|
||||
})}
|
||||
/>
|
||||
<P>
|
||||
<Trans i18nKey="login.or-login">
|
||||
Have an account already? <A href={{ pathname: "/login", query: { apiUrl } }}>Log in</A>.
|
||||
|
Loading…
x
Reference in New Issue
Block a user