mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-28 12:27:51 -04:00
Add disable registration option
This commit is contained in:
parent
9affcd6c16
commit
d2ea4da097
@ -6,6 +6,9 @@ RSA_PRIVATE_KEY_PATH=""
|
||||
|
||||
PROFILE_PICTURE_PATH="/profile_pictures"
|
||||
|
||||
# If true, POST /users registration is disabled and returns 403.
|
||||
DISABLE_REGISTRATION=false
|
||||
|
||||
# json object with the claims to add to every jwt (this is read when creating a new user)
|
||||
EXTRA_CLAIMS='{}'
|
||||
# json object with the claims to add to every jwt of the FIRST user (this can be used to mark the first user as admin).
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -24,18 +25,19 @@ import (
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
JwtPrivateKey *rsa.PrivateKey
|
||||
JwtPublicKey *rsa.PublicKey
|
||||
JwtKid string
|
||||
PublicUrl string
|
||||
OidcProviders map[string]OidcProviderConfig
|
||||
DefaultClaims jwt.MapClaims
|
||||
FirstUserClaims jwt.MapClaims
|
||||
GuestClaims jwt.MapClaims
|
||||
ProtectedClaims []string
|
||||
ExpirationDelay time.Duration
|
||||
EnvApiKeys []ApiKeyWToken
|
||||
ProfilePicturePath string
|
||||
JwtPrivateKey *rsa.PrivateKey
|
||||
JwtPublicKey *rsa.PublicKey
|
||||
JwtKid string
|
||||
PublicUrl string
|
||||
OidcProviders map[string]OidcProviderConfig
|
||||
DefaultClaims jwt.MapClaims
|
||||
FirstUserClaims jwt.MapClaims
|
||||
GuestClaims jwt.MapClaims
|
||||
ProtectedClaims []string
|
||||
ExpirationDelay time.Duration
|
||||
EnvApiKeys []ApiKeyWToken
|
||||
ProfilePicturePath string
|
||||
DisableRegistration bool
|
||||
}
|
||||
|
||||
type OidcAuthMethod string
|
||||
@ -76,6 +78,12 @@ func LoadConfiguration(ctx context.Context, db *dbc.Queries) (*Configuration, er
|
||||
"/profile_pictures",
|
||||
)
|
||||
|
||||
disableRegistration, err := strconv.ParseBool(cmp.Or(os.Getenv("DISABLE_REGISTRATION"), "false"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid DISABLE_REGISTRATION value: %w", err)
|
||||
}
|
||||
ret.DisableRegistration = disableRegistration
|
||||
|
||||
claims := os.Getenv("EXTRA_CLAIMS")
|
||||
if claims != "" {
|
||||
err := json.Unmarshal([]byte(claims), &ret.DefaultClaims)
|
||||
|
||||
10
auth/oidc.go
10
auth/oidc.go
@ -530,8 +530,9 @@ func (h *Handler) OidcUnlink(c *echo.Context) error {
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
PublicUrl string `json:"publicUrl"`
|
||||
Oidc map[string]OidcInfo `json:"oidc"`
|
||||
PublicUrl string `json:"publicUrl"`
|
||||
AllowRegister bool `json:"allowRegister"`
|
||||
Oidc map[string]OidcInfo `json:"oidc"`
|
||||
}
|
||||
|
||||
type OidcInfo struct {
|
||||
@ -547,8 +548,9 @@ type OidcInfo struct {
|
||||
// @Router /info [get]
|
||||
func (h *Handler) Info(c *echo.Context) error {
|
||||
ret := ServerInfo{
|
||||
PublicUrl: h.config.PublicUrl,
|
||||
Oidc: make(map[string]OidcInfo),
|
||||
PublicUrl: h.config.PublicUrl,
|
||||
AllowRegister: !h.config.DisableRegistration,
|
||||
Oidc: make(map[string]OidcInfo),
|
||||
}
|
||||
for _, provider := range h.config.OidcProviders {
|
||||
ret.Oidc[provider.Id] = OidcInfo{
|
||||
|
||||
@ -159,9 +159,14 @@ func (h *Handler) GetMe(c *echo.Context) error {
|
||||
// @Param user body RegisterDto false "Registration informations"
|
||||
// @Success 201 {object} SessionWToken
|
||||
// @Success 409 {object} KError "Duplicated email or username"
|
||||
// @Failure 403 {object} KError "Registrations are disabled"
|
||||
// @Failure 422 {object} KError "Invalid register body"
|
||||
// @Router /users [post]
|
||||
func (h *Handler) Register(c *echo.Context) error {
|
||||
if h.config.DisableRegistration {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Registrations are disabled")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
var req RegisterDto
|
||||
err := c.Bind(&req)
|
||||
|
||||
@ -250,7 +250,9 @@
|
||||
"or-login": "Have an account already? <1>Log in</1>.",
|
||||
"password-no-match": "Passwords do not match.",
|
||||
"delete": "Delete your account",
|
||||
"delete-confirmation": "This action can't be reverted. Are you sure?"
|
||||
"delete-confirmation": "This action can't be reverted. Are you sure?",
|
||||
"register-disabled": "Registrations are disabled.",
|
||||
"register-disabled-oidc": "Password registration is disabled. Use OIDC."
|
||||
},
|
||||
"downloads": {
|
||||
"empty": "Nothing downloaded yet, start browsing for something you like",
|
||||
|
||||
@ -4,6 +4,7 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { A, Button, H1, Input, P } from "~/primitives";
|
||||
import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { useFetch } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { FormPage } from "./form";
|
||||
import { login } from "./logic";
|
||||
@ -20,46 +21,48 @@ export const LoginPage = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { data: info } = useFetch(OidcLogin.query(apiUrl));
|
||||
|
||||
if (Platform.OS !== "web" && !apiUrl) return <ServerUrlPage />;
|
||||
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<H1 className="pb-4">{t("login.login")}</H1>
|
||||
<OidcLogin apiUrl={apiUrl} error={error}>
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
{error && <P className="text-red-500 dark:text-red-500">{error}</P>}
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
onPress={async () => {
|
||||
const { error } = await login("login", {
|
||||
login: username,
|
||||
password,
|
||||
apiUrl,
|
||||
});
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/");
|
||||
}}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
{error && <P className="text-red-500 dark:text-red-500">{error}</P>}
|
||||
<Button
|
||||
text={t("login.login")}
|
||||
onPress={async () => {
|
||||
const { error } = await login("login", {
|
||||
login: username,
|
||||
password,
|
||||
apiUrl,
|
||||
});
|
||||
setError(error);
|
||||
if (error) return;
|
||||
router.replace("/");
|
||||
}}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
{info?.allowRegister !== false && (
|
||||
<P>
|
||||
<Trans i18nKey="login.or-register">
|
||||
Don’t have an account?
|
||||
<A href={`/register?apiUrl=${apiUrl}`}>Register</A>.
|
||||
</Trans>
|
||||
</P>
|
||||
</OidcLogin>
|
||||
)}
|
||||
</FormPage>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +1,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image, Platform, View } from "react-native";
|
||||
import { z } from "zod/v4";
|
||||
import { Image, View } from "react-native";
|
||||
import { AuthInfo } from "~/models/auth-info";
|
||||
import { Button, HR, Link, P, Skeleton } from "~/primitives";
|
||||
import { Fetch, type QueryIdentifier } from "~/query";
|
||||
|
||||
export const OidcLogin = ({
|
||||
apiUrl,
|
||||
children,
|
||||
error,
|
||||
}: {
|
||||
apiUrl: string;
|
||||
children: ReactNode;
|
||||
error?: string;
|
||||
}) => {
|
||||
export const OidcLogin = ({ apiUrl }: { apiUrl: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const or = (
|
||||
@ -24,7 +14,6 @@ export const OidcLogin = ({
|
||||
<P>{t("misc.or")}</P>
|
||||
<HR className="grow" />
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -54,11 +43,7 @@ export const OidcLogin = ({
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{info.allowRegister
|
||||
? or
|
||||
: error && (
|
||||
<P className="text-red-500 dark:text-red-500">{error}</P>
|
||||
)}
|
||||
{or}
|
||||
</>
|
||||
)}
|
||||
Loader={() => (
|
||||
|
||||
@ -4,6 +4,7 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import { A, Button, H1, Input, P } from "~/primitives";
|
||||
import { defaultApiUrl } from "~/providers/account-provider";
|
||||
import { useFetch } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { FormPage } from "./form";
|
||||
import { login } from "./logic";
|
||||
@ -21,63 +22,84 @@ export const RegisterPage = () => {
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { data: info } = useFetch(OidcLogin.query(apiUrl));
|
||||
|
||||
if (Platform.OS !== "web" && !apiUrl) return <ServerUrlPage />;
|
||||
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<H1 className="pb-4">{t("login.register")}</H1>
|
||||
<OidcLogin apiUrl={apiUrl}>
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
/>
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.email")}</P>
|
||||
<Input autoComplete="email" onChangeText={(value) => setEmail(value)} />
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
|
||||
{password !== confirm && (
|
||||
<P className="text-red-500 dark:text-red-500">
|
||||
{t("login.password-no-match")}
|
||||
</P>
|
||||
)}
|
||||
{error && <P className="text-red-500 dark:text-red-500">{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("/");
|
||||
}}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
if (info?.allowRegister === false) {
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<H1 className="pb-4">{t("login.register")}</H1>
|
||||
<P className="mb-6">
|
||||
{t(
|
||||
Object.values(info.oidc).length > 0
|
||||
? "login.register-disabled-oidc"
|
||||
: "login.register-disabled",
|
||||
)}
|
||||
</P>
|
||||
<P>
|
||||
<Trans i18nKey="login.or-login">
|
||||
Have an account already?
|
||||
<A href={`/login?apiUrl=${apiUrl}`}>Log in</A>.
|
||||
</Trans>
|
||||
</P>
|
||||
</OidcLogin>
|
||||
</FormPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormPage apiUrl={apiUrl!}>
|
||||
<H1 className="pb-4">{t("login.register")}</H1>
|
||||
<OidcLogin apiUrl={apiUrl} />
|
||||
<P className="pl-2">{t("login.username")}</P>
|
||||
<Input
|
||||
autoComplete="username"
|
||||
onChangeText={(value) => setUsername(value)}
|
||||
/>
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.email")}</P>
|
||||
<Input autoComplete="email" onChangeText={(value) => setEmail(value)} />
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
|
||||
<P className="pt-2 pl-2">{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="new-password"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
|
||||
{password !== confirm && (
|
||||
<P className="text-red-500 dark:text-red-500">
|
||||
{t("login.password-no-match")}
|
||||
</P>
|
||||
)}
|
||||
{error && <P className="text-red-500 dark:text-red-500">{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("/");
|
||||
}}
|
||||
className="m-2 my-6 w-60 self-center"
|
||||
/>
|
||||
<P>
|
||||
<Trans i18nKey="login.or-login">
|
||||
Have an account already?
|
||||
<A href={`/login?apiUrl=${apiUrl}`}>Log in</A>.
|
||||
</Trans>
|
||||
</P>
|
||||
</FormPage>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user