Add disable registration option

This commit is contained in:
Zoe Roux 2026-03-26 22:55:02 +01:00
parent 9affcd6c16
commit d2ea4da097
No known key found for this signature in database
8 changed files with 142 additions and 112 deletions

View File

@ -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).

View File

@ -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)

View File

@ -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{

View File

@ -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)

View File

@ -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",

View File

@ -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">
Dont have an account?
<A href={`/register?apiUrl=${apiUrl}`}>Register</A>.
</Trans>
</P>
</OidcLogin>
)}
</FormPage>
);
};

View File

@ -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={() => (

View File

@ -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>
);
};