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)
|
# http route prefix (will listen to $KYOO_PREFIX/movie for example)
|
||||||
KYOO_PREFIX=""
|
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 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
|
# 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:
|
# 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.
|
/// Check if kyoo's setup is finished.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SetupStep SetupStatus { get; set; }
|
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
|
public class OidcInfo
|
||||||
|
@ -26,6 +26,7 @@ using Kyoo.Abstractions.Models.Attributes;
|
|||||||
using Kyoo.Abstractions.Models.Exceptions;
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Abstractions.Models.Permissions;
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Authentication.Attributes;
|
||||||
using Kyoo.Authentication.Models;
|
using Kyoo.Authentication.Models;
|
||||||
using Kyoo.Authentication.Models.DTO;
|
using Kyoo.Authentication.Models.DTO;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
@ -206,6 +207,7 @@ public class AuthApi(
|
|||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
[DisableOnEnvVar("AUTHENTICATION_DISABLE_PASSWORD_LOGIN")]
|
||||||
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
|
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
|
||||||
{
|
{
|
||||||
User? user = await users.GetOrDefault(
|
User? user = await users.GetOrDefault(
|
||||||
@ -241,6 +243,7 @@ public class AuthApi(
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
||||||
|
[DisableOnEnvVar("AUTHENTICATION_DISABLE_USER_REGISTRATION")]
|
||||||
public async Task<ActionResult<JwtToken>> Register([FromBody] RegisterRequest request)
|
public async Task<ActionResult<JwtToken>> Register([FromBody] RegisterRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -23,6 +23,7 @@ using Kyoo.Abstractions.Models.Attributes;
|
|||||||
using Kyoo.Authentication.Models;
|
using Kyoo.Authentication.Models;
|
||||||
using Kyoo.Core.Controllers;
|
using Kyoo.Core.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Views;
|
namespace Kyoo.Authentication.Views;
|
||||||
@ -33,7 +34,8 @@ namespace Kyoo.Authentication.Views;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("info")]
|
[Route("info")]
|
||||||
[ApiDefinition("Info", Group = UsersGroup)]
|
[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()
|
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),
|
.ToDictionary(x => x.Key, x => x.Value),
|
||||||
SetupStatus = await info.GetSetupStep(),
|
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.
|
* Check if kyoo's setup is finished.
|
||||||
*/
|
*/
|
||||||
setupStatus: z.nativeEnum(SetupStep),
|
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) => {
|
.transform((x) => {
|
||||||
const baseUrl = Platform.OS === "web" ? x.publicUrl : "kyoo://";
|
const baseUrl = Platform.OS === "web" ? x.publicUrl : "kyoo://";
|
||||||
|
@ -18,11 +18,10 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { A, Button, H1, Input, P, ts } from "@kyoo/primitives";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||||
@ -35,6 +34,11 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
|||||||
apiUrl,
|
apiUrl,
|
||||||
error: initialError,
|
error: initialError,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data } = useFetch({
|
||||||
|
path: ["info"],
|
||||||
|
parser: ServerInfoP,
|
||||||
|
});
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState<string | undefined>(initialError);
|
const [error, setError] = useState<string | undefined>(initialError);
|
||||||
@ -53,43 +57,47 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
|||||||
return (
|
return (
|
||||||
<FormPage apiUrl={apiUrl}>
|
<FormPage apiUrl={apiUrl}>
|
||||||
<H1>{t("login.login")}</H1>
|
<H1>{t("login.login")}</H1>
|
||||||
<OidcLogin apiUrl={apiUrl} />
|
<OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} />
|
||||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
{data?.passwordLoginEnabled && (
|
||||||
<Input
|
<>
|
||||||
autoComplete="username"
|
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
||||||
variant="big"
|
<Input
|
||||||
onChangeText={(value) => setUsername(value)}
|
autoComplete="username"
|
||||||
autoCapitalize="none"
|
variant="big"
|
||||||
/>
|
onChangeText={(value) => setUsername(value)}
|
||||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
autoCapitalize="none"
|
||||||
<PasswordInput
|
/>
|
||||||
autoComplete="password"
|
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||||
variant="big"
|
<PasswordInput
|
||||||
onChangeText={(value) => setPassword(value)}
|
autoComplete="password"
|
||||||
/>
|
variant="big"
|
||||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
onChangeText={(value) => setPassword(value)}
|
||||||
<Button
|
/>
|
||||||
text={t("login.login")}
|
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||||
onPress={async () => {
|
<Button
|
||||||
const { error } = await login("login", {
|
text={t("login.login")}
|
||||||
username,
|
onPress={async () => {
|
||||||
password,
|
const { error } = await login("login", {
|
||||||
apiUrl,
|
username,
|
||||||
});
|
password,
|
||||||
setError(error);
|
apiUrl,
|
||||||
if (error) return;
|
});
|
||||||
router.replace("/", undefined, {
|
setError(error);
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
if (error) return;
|
||||||
});
|
router.replace("/", undefined, {
|
||||||
}}
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||||
{...css({
|
});
|
||||||
m: ts(1),
|
}}
|
||||||
width: px(250),
|
{...css({
|
||||||
maxWidth: percent(100),
|
m: ts(1),
|
||||||
alignSelf: "center",
|
width: px(250),
|
||||||
mY: ts(3),
|
maxWidth: percent(100),
|
||||||
})}
|
alignSelf: "center",
|
||||||
/>
|
mY: ts(3),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<P>
|
<P>
|
||||||
<Trans i18nKey="login.or-register">
|
<Trans i18nKey="login.or-register">
|
||||||
Don’t have an account? <A href={{ pathname: "/register", query: { apiUrl } }}>Register</A>
|
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 { percent, rem, useYoshiki } from "yoshiki/native";
|
||||||
import { ErrorView } from "../errors";
|
import { ErrorView } from "../errors";
|
||||||
|
|
||||||
export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => {
|
export const OidcLogin = ({ apiUrl, hideOr }: { apiUrl?: string; hideOr?: boolean }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, error } = useFetch({ options: { apiUrl }, ...OidcLogin.query() });
|
const { data, error } = useFetch({ options: { apiUrl }, ...OidcLogin.query() });
|
||||||
@ -76,6 +76,7 @@ export const OidcLogin = ({ apiUrl }: { apiUrl?: string }) => {
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
display: hideOr ? "none" : undefined,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<HR {...css({ flexGrow: 1 })} />
|
<HR {...css({ flexGrow: 1 })} />
|
||||||
|
@ -18,11 +18,10 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { A, Button, H1, Input, P, ts } from "@kyoo/primitives";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||||
@ -32,6 +31,11 @@ import { OidcLogin } from "./oidc";
|
|||||||
import { PasswordInput } from "./password-input";
|
import { PasswordInput } from "./password-input";
|
||||||
|
|
||||||
export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
||||||
|
const { data } = useFetch({
|
||||||
|
path: ["info"],
|
||||||
|
parser: ServerInfoP,
|
||||||
|
});
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@ -52,50 +56,58 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
|||||||
return (
|
return (
|
||||||
<FormPage apiUrl={apiUrl}>
|
<FormPage apiUrl={apiUrl}>
|
||||||
<H1>{t("login.register")}</H1>
|
<H1>{t("login.register")}</H1>
|
||||||
<OidcLogin apiUrl={apiUrl} />
|
<OidcLogin apiUrl={apiUrl} hideOr={!data?.passwordLoginEnabled} />
|
||||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.username")}</P>
|
{data?.registrationEnabled && (
|
||||||
<Input autoComplete="username" variant="big" onChangeText={(value) => setUsername(value)} />
|
<>
|
||||||
|
<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>
|
<P {...css({ paddingLeft: ts(1) })}>{t("login.email")}</P>
|
||||||
<Input autoComplete="email" variant="big" onChangeText={(value) => setEmail(value)} />
|
<Input autoComplete="email" variant="big" onChangeText={(value) => setEmail(value)} />
|
||||||
|
|
||||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
<P {...css({ paddingLeft: ts(1) })}>{t("login.password")}</P>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
autoComplete="password-new"
|
autoComplete="password-new"
|
||||||
variant="big"
|
variant="big"
|
||||||
onChangeText={(value) => setPassword(value)}
|
onChangeText={(value) => setPassword(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<P {...css({ paddingLeft: ts(1) })}>{t("login.confirm")}</P>
|
<P {...css({ paddingLeft: ts(1) })}>{t("login.confirm")}</P>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
autoComplete="password-new"
|
autoComplete="password-new"
|
||||||
variant="big"
|
variant="big"
|
||||||
onChangeText={(value) => setConfirm(value)}
|
onChangeText={(value) => setConfirm(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{password !== confirm && (
|
{password !== confirm && (
|
||||||
<P {...css({ color: (theme) => theme.colors.red })}>{t("login.password-no-match")}</P>
|
<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>
|
<P>
|
||||||
<Trans i18nKey="login.or-login">
|
<Trans i18nKey="login.or-login">
|
||||||
Have an account already? <A href={{ pathname: "/login", query: { apiUrl } }}>Log in</A>.
|
Have an account already? <A href={{ pathname: "/login", query: { apiUrl } }}>Log in</A>.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user