Captcha trait

This commit is contained in:
Roman 2024-05-03 19:05:43 +02:00
parent 3a639d9a3c
commit 76cd92ee0e
4 changed files with 162 additions and 39 deletions

@ -0,0 +1,30 @@
<?php
namespace Core\API\Traits;
use Core\API\Parameter\StringType;
use Core\API\VerifyCaptcha;
use Core\Objects\Context;
trait Captcha {
function addCaptchaParameters(array &$parameters): void {
$settings = $this->context->getSettings();
if ($settings->isCaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
}
function checkCaptcha(string $action): bool {
$settings = $this->context->getSettings();
if ($settings->isCaptchaEnabled()) {
$captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->context);
if (!$req->execute(array("captcha" => $captcha, "action" => $action))) {
return $this->createError($req->getLastError());
}
}
return true;
}
}

@ -124,9 +124,9 @@ namespace Core\API\User {
use Core\API\Parameter\Parameter; use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType; use Core\API\Parameter\StringType;
use Core\API\Template\Render; use Core\API\Template\Render;
use Core\API\Traits\Captcha;
use Core\API\Traits\Pagination; use Core\API\Traits\Pagination;
use Core\API\UserAPI; use Core\API\UserAPI;
use Core\API\VerifyCaptcha;
use Core\Driver\SQL\Condition\CondBool; use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondLike; use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondOr; use Core\Driver\SQL\Condition\CondOr;
@ -716,6 +716,8 @@ namespace Core\API\User {
class Register extends UserAPI { class Register extends UserAPI {
use Captcha;
public function __construct(Context $context, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
$parameters = array( $parameters = array(
"username" => new StringType("username", 32), "username" => new StringType("username", 32),
@ -724,10 +726,7 @@ namespace Core\API\User {
"confirmPassword" => new StringType("confirmPassword"), "confirmPassword" => new StringType("confirmPassword"),
); );
$settings = $context->getSettings(); $this->addCaptchaParameters($parameters);
if ($settings->isCaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
parent::__construct($context, $externalCall, $parameters); parent::__construct($context, $externalCall, $parameters);
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
@ -745,12 +744,8 @@ namespace Core\API\User {
return $this->createError("User Registration is not enabled."); return $this->createError("User Registration is not enabled.");
} }
if ($settings->isCaptchaEnabled()) { if (!$this->checkCaptcha("register")) {
$captcha = $this->getParam("captcha"); return false;
$req = new VerifyCaptcha($this->context);
if (!$req->execute(array("captcha" => $captcha, "action" => "register"))) {
return $this->createError($req->getLastError());
}
} }
$username = $this->getParam("username"); $username = $this->getParam("username");
@ -840,7 +835,8 @@ namespace Core\API\User {
'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL), 'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL),
'password' => new StringType('password', -1, true, NULL), 'password' => new StringType('password', -1, true, NULL),
'groups' => new ArrayType('groups', Parameter::TYPE_INT, true, true, NULL), 'groups' => new ArrayType('groups', Parameter::TYPE_INT, true, true, NULL),
'confirmed' => new Parameter('confirmed', Parameter::TYPE_BOOLEAN, true, NULL) 'confirmed' => new Parameter('confirmed', Parameter::TYPE_BOOLEAN, true, NULL),
'active' => new Parameter('active', Parameter::TYPE_BOOLEAN, true, NULL)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
@ -865,6 +861,7 @@ namespace Core\API\User {
$password = $this->getParam("password"); $password = $this->getParam("password");
$groups = $this->getParam("groups"); $groups = $this->getParam("groups");
$confirmed = $this->getParam("confirmed"); $confirmed = $this->getParam("confirmed");
$active = $this->getParam("active");
$email = (!is_null($email) && empty($email)) ? null : $email; $email = (!is_null($email) && empty($email)) ? null : $email;
@ -918,13 +915,22 @@ namespace Core\API\User {
if (!is_null($confirmed)) { if (!is_null($confirmed)) {
if ($id === $currentUser->getId() && $confirmed === false) { if ($id === $currentUser->getId() && $confirmed === false) {
return $this->createError("Cannot make own account unconfirmed."); return $this->createError("Cannot change confirmed flag on own account.");
} else { } else {
$user->confirmed = $confirmed; $user->confirmed = $confirmed;
$columnsToUpdate[] = "confirmed"; $columnsToUpdate[] = "confirmed";
} }
} }
if (!is_null($active)) {
if ($id === $currentUser->getId() && $active === false) {
return $this->createError("Cannot change active flag on own account.");
} else {
$user->active = $active;
$columnsToUpdate[] = "active";
}
}
if (empty($columnsToUpdate) || $user->save($sql, $columnsToUpdate)) { if (empty($columnsToUpdate) || $user->save($sql, $columnsToUpdate)) {
$deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id); $deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id);
@ -995,16 +1001,15 @@ namespace Core\API\User {
} }
class RequestPasswordReset extends UserAPI { class RequestPasswordReset extends UserAPI {
use Captcha;
public function __construct(Context $context, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
$parameters = array( $parameters = [
'email' => new Parameter('email', Parameter::TYPE_EMAIL), 'email' => new Parameter('email', Parameter::TYPE_EMAIL),
); ];
$settings = $context->getSettings();
if ($settings->isCaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
$this->addCaptchaParameters($parameters);
parent::__construct($context, $externalCall, $parameters); parent::__construct($context, $externalCall, $parameters);
} }
@ -1019,12 +1024,8 @@ namespace Core\API\User {
return $this->createError("The mail service is not enabled, please contact the server administration."); return $this->createError("The mail service is not enabled, please contact the server administration.");
} }
if ($settings->isCaptchaEnabled()) { if (!$this->checkCaptcha("resetPassword")) {
$captcha = $this->getParam("captcha"); return false;
$req = new VerifyCaptcha($this->context);
if (!$req->execute(array("captcha" => $captcha, "action" => "resetPassword"))) {
return $this->createError($req->getLastError());
}
} }
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
@ -1088,16 +1089,15 @@ namespace Core\API\User {
} }
class ResendConfirmEmail extends UserAPI { class ResendConfirmEmail extends UserAPI {
use Captcha;
public function __construct(Context $context, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
$parameters = array( $parameters = array(
'email' => new Parameter('email', Parameter::TYPE_EMAIL), 'email' => new Parameter('email', Parameter::TYPE_EMAIL),
); );
$settings = $context->getSettings(); $this->addCaptchaParameters($parameters);
if ($settings->isCaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
parent::__construct($context, $externalCall, $parameters); parent::__construct($context, $externalCall, $parameters);
} }
@ -1108,12 +1108,8 @@ namespace Core\API\User {
} }
$settings = $this->context->getSettings(); $settings = $this->context->getSettings();
if ($settings->isCaptchaEnabled()) { if (!$this->checkCaptcha("resendConfirmation")) {
$captcha = $this->getParam("captcha"); return false;
$req = new VerifyCaptcha($this->context);
if (!$req->execute(array("captcha" => $captcha, "action" => "resendConfirmation"))) {
return $this->createError($req->getLastError());
}
} }
$email = $this->getParam("email"); $email = $this->getParam("email");

@ -9,7 +9,7 @@ return [
"search" => "Suche", "search" => "Suche",
"search_query" => "Suchanfrage", "search_query" => "Suchanfrage",
"no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen", "no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen",
"timestamp_placeholder" => "Datum und Zeitpunk Auswählen zum Filtern", "timestamp_placeholder" => "Datum und Zeitpunkt auswählen zum Filtern",
"hide_details" => "Details verstecken", "hide_details" => "Details verstecken",
"show_details" => "Details zeigen", "show_details" => "Details zeigen",

@ -1,15 +1,32 @@
import {Link, useNavigate, useParams} from "react-router-dom"; import {Link, useNavigate, useParams} from "react-router-dom";
import {useCallback, useContext, useEffect, useState} from "react"; import {useCallback, useContext, useEffect, useState} from "react";
import {CircularProgress} from "@mui/material"; import {
Box,
Button,
Checkbox,
CircularProgress,
FormControl,
FormControlLabel,
FormLabel,
TextField
} from "@mui/material";
import {LocaleContext} from "shared/locale"; import {LocaleContext} from "shared/locale";
import * as React from "react"; import * as React from "react";
import ViewContent from "../../elements/view-content"; import ViewContent from "../../elements/view-content";
import FormGroup from "../../elements/form-group";
import ButtonBar from "../../elements/button-bar";
import {RestartAlt, Save} from "@mui/icons-material";
import {parseBool} from "shared/util";
import SpacedFormGroup from "../../elements/form-group";
export default function UserEditView(props) { export default function UserEditView(props) {
// meta
const { api, showDialog } = props; const { api, showDialog } = props;
const { userId } = useParams(); const { userId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
// data
const isNewUser = userId === "new"; const isNewUser = userId === "new";
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext); const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
const [fetchUser, setFetchUser] = useState(!isNewUser); const [fetchUser, setFetchUser] = useState(!isNewUser);
@ -20,8 +37,13 @@ export default function UserEditView(props) {
password: "", password: "",
groups: [], groups: [],
confirmed: false, confirmed: false,
active: true,
} : null); } : null);
// ui
const [hasChanged, setChanged] = useState(isNewUser);
const [isSaving, setSaving] = useState(false);
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general", "account"], currentLocale).then(data => { requestModules(props.api, ["general", "account"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
@ -30,6 +52,14 @@ export default function UserEditView(props) {
}); });
}, [currentLocale]); }, [currentLocale]);
const onReset = useCallback(() => {
}, []);
const onSaveUser = useCallback(() => {
}, []);
const onFetchUser = useCallback((force = false) => { const onFetchUser = useCallback((force = false) => {
if (!isNewUser && (force || fetchUser)) { if (!isNewUser && (force || fetchUser)) {
setFetchUser(false); setFetchUser(false);
@ -46,6 +76,10 @@ export default function UserEditView(props) {
} }
}, [api, showDialog, fetchUser, isNewUser, userId, user]); }, [api, showDialog, fetchUser, isNewUser, userId, user]);
const onChangeValue = useCallback((name, value) => {
}, []);
useEffect(() => { useEffect(() => {
if (!isNewUser) { if (!isNewUser) {
onFetchUser(true); onFetchUser(true);
@ -61,6 +95,69 @@ export default function UserEditView(props) {
<Link key={"users"} to={"/admin/users"}>User</Link>, <Link key={"users"} to={"/admin/users"}>User</Link>,
<span key={"action"}>{isNewUser ? "New" : "Edit"}</span> <span key={"action"}>{isNewUser ? "New" : "Edit"}</span>
]}> ]}>
<Box>
<FormGroup>
<FormLabel>{L("account.name")}</FormLabel>
<FormControl>
<TextField size={"small"} variant={"outlined"}
value={user.name}
onChange={e => setUser({...user, name: e.target.value})} />
</FormControl>
</FormGroup>
<FormGroup>
<FormLabel>{L("account.full_name")}</FormLabel>
<FormControl>
<TextField size={"small"} variant={"outlined"}
value={user.fullName}
onChange={e => setUser({...user, fullName: e.target.value})} />
</FormControl>
</FormGroup>
<FormGroup>
<FormLabel>{L("account.email")}</FormLabel>
<FormControl>
<TextField size={"small"} variant={"outlined"}
value={user.email}
type={"email"}
onChange={e => setUser({...user, email: e.target.value})} />
</FormControl>
</FormGroup>
{ !isNewUser ?
<>
<FormGroup>
<FormControlLabel
control={<Checkbox
checked={!!user.active}
onChange={(e, v) => onChangeValue("active", v)} />}
label={L("account.active")} />
</FormGroup>
<FormGroup>
<FormControlLabel
control={<Checkbox
checked={!!user.confirmed}
onChange={(e, v) => onChangeValue("confirmed", v)} />}
label={L("account.confirmed")} />
</FormGroup>
</> : <>
</>
}
</Box>
<ButtonBar>
<Button color={"primary"}
onClick={onSaveUser}
disabled={isSaving || !(isNewUser ? api.hasPermission("user/create") : api.hasPermission("user/edit"))}
startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}>
{isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))}
</Button>
<Button color={"error"}
onClick={onReset}
disabled={isSaving}
startIcon={<RestartAlt />}
variant={"outlined"} title={L("general.reset")}>
{L("general.reset")}
</Button>
</ButtonBar>
</ViewContent> </ViewContent>
} }