small changes
This commit is contained in:
		
							parent
							
								
									327f570316
								
							
						
					
					
						commit
						136ad48a5e
					
				| @ -16,6 +16,7 @@ namespace Core\API\ApiKey { | ||||
| 
 | ||||
|   use Core\API\ApiKeyAPI; | ||||
|   use Core\API\Parameter\Parameter; | ||||
|   use Core\API\Traits\Pagination; | ||||
|   use Core\Driver\SQL\Condition\Compare; | ||||
|   use Core\Driver\SQL\Condition\CondAnd; | ||||
|   use Core\Driver\SQL\Query\Insert; | ||||
| @ -32,17 +33,16 @@ namespace Core\API\ApiKey { | ||||
| 
 | ||||
|     public function _execute(): bool { | ||||
|       $sql = $this->context->getSQL(); | ||||
|       $currentUser = $this->context->getUser(); | ||||
| 
 | ||||
|       $apiKey = new ApiKey(); | ||||
|       $apiKey->apiKey = generateRandomString(64); | ||||
|       $apiKey->validUntil = (new \DateTime())->modify("+30 DAY"); | ||||
|       $apiKey->user = $this->context->getUser(); | ||||
| 
 | ||||
|       $apiKey = ApiKey::create($currentUser); | ||||
|       $this->success = $apiKey->save($sql); | ||||
|       $this->lastError = $sql->getLastError(); | ||||
| 
 | ||||
|       if ($this->success) { | ||||
|         $this->result["api_key"] = $apiKey->jsonSerialize(); | ||||
|         $this->result["apiKey"] = $apiKey->jsonSerialize( | ||||
|           ["id", "validUntil", "token", "active"] | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       return $this->success; | ||||
| @ -55,10 +55,13 @@ namespace Core\API\ApiKey { | ||||
| 
 | ||||
|   class Fetch extends ApiKeyAPI { | ||||
| 
 | ||||
|     use Pagination; | ||||
| 
 | ||||
|     public function __construct(Context $context, $externalCall = false) { | ||||
|       parent::__construct($context, $externalCall, array( | ||||
|         "showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true) | ||||
|       )); | ||||
|       $params = $this->getPaginationParameters(["token", "validUntil", "active"]); | ||||
|       $params["showActiveOnly"] = new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true); | ||||
| 
 | ||||
|       parent::__construct($context, $externalCall, $params); | ||||
|       $this->loginRequired = true; | ||||
|     } | ||||
| 
 | ||||
| @ -74,14 +77,18 @@ namespace Core\API\ApiKey { | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       $apiKeys = ApiKey::findAll($sql, $condition); | ||||
|       $this->success = ($apiKeys !== FALSE); | ||||
|       if (!$this->initPagination($sql, ApiKey::class, $condition)) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       $apiKeys = $this->createPaginationQuery($sql)->execute(); | ||||
|       $this->success = ($apiKeys !== FALSE && $apiKeys !== null); | ||||
|       $this->lastError = $sql->getLastError(); | ||||
| 
 | ||||
|       if ($this->success) { | ||||
|         $this->result["api_keys"] = array(); | ||||
|         $this->result["apiKeys"] = []; | ||||
|         foreach($apiKeys as $apiKey) { | ||||
|           $this->result["api_keys"][$apiKey->getId()] = $apiKey->jsonSerialize(); | ||||
|           $this->result["apiKeys"][] = $apiKey->jsonSerialize(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ namespace Core\API { | ||||
|         $settings = $req->getResult()["settings"]; | ||||
| 
 | ||||
|         if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") { | ||||
|           $this->createError("Mail is not configured yet."); | ||||
|           $this->createError("Mailing is not configured on this server yet."); | ||||
|           return null; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -252,13 +252,18 @@ namespace Core\API\TFA { | ||||
|       // $domain = "localhost";
 | ||||
| 
 | ||||
|       if (!$clientDataJSON || !$attestationObjectRaw) { | ||||
|         $challenge = null; | ||||
|         if ($twoFactorToken) { | ||||
|           if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) { | ||||
|           if ($twoFactorToken->isConfirmed()) { | ||||
|             return $this->createError("You already added a two factor token"); | ||||
|           } else { | ||||
|           } else if ($twoFactorToken instanceof KeyBasedTwoFactorToken) { | ||||
|             $challenge = $twoFactorToken->getChallenge(); | ||||
|           } else { | ||||
|             $twoFactorToken->delete($sql); | ||||
|           } | ||||
|         } else { | ||||
|         } | ||||
| 
 | ||||
|         if ($challenge === null) { | ||||
|           $twoFactorToken = KeyBasedTwoFactorToken::create(); | ||||
|           $challenge = $twoFactorToken->getChallenge(); | ||||
|           $this->success = ($twoFactorToken->save($sql) !== false); | ||||
| @ -307,6 +312,10 @@ namespace Core\API\TFA { | ||||
| 
 | ||||
|         $this->success = $twoFactorToken->confirmKeyBased($sql, base64_encode($authData->getCredentialID()), $publicKey) !== false; | ||||
|         $this->lastError = $sql->getLastError(); | ||||
| 
 | ||||
|         if ($this->success) { | ||||
|           $this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return $this->success; | ||||
|  | ||||
| @ -1223,6 +1223,8 @@ namespace Core\API\User { | ||||
|       $gpgKey = $currentUser->getGPG(); | ||||
|       if ($gpgKey) { | ||||
|         return $this->createError("You already added a GPG key to your account."); | ||||
|       } else if (!$currentUser->getEmail()) { | ||||
|         return $this->createError("You do not have an e-mail address"); | ||||
|       } | ||||
| 
 | ||||
|       // fix key first, enforce a newline after
 | ||||
| @ -1280,7 +1282,7 @@ namespace Core\API\User { | ||||
|       if ($this->success) { | ||||
|         $currentUser->gpgKey = $gpgKey; | ||||
|         if ($currentUser->save($sql, ["gpgKey"])) { | ||||
|           $this->result["gpg"] = $gpgKey->jsonSerialize(); | ||||
|           $this->result["gpgKey"] = $gpgKey->jsonSerialize(); | ||||
|         } else { | ||||
|           return $this->createError("Error updating user details: " . $sql->getLastError()); | ||||
|         } | ||||
|  | ||||
| @ -149,7 +149,7 @@ abstract class SQL { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     $logLevel = Logger::LOG_LEVEL_DEBUG; | ||||
|     $logLevel = Logger::LOG_LEVEL_ERROR; | ||||
|     if ($query instanceof Insert && $query->getTableName() === "SystemLog") { | ||||
|       $logLevel = Logger::LOG_LEVEL_NONE; | ||||
|     } | ||||
|  | ||||
| @ -44,4 +44,5 @@ return [ | ||||
|   "confirm_error" => "Fehler beim Bestätigen der E-Mail Adresse", | ||||
|   "gpg_key" => "GPG-Schlüssel", | ||||
|   "2fa_token" => "Zwei-Faktor Authentifizierung (2FA)", | ||||
|   "profile_picture_of" => "Profilbild von", | ||||
| ]; | ||||
| @ -44,4 +44,5 @@ return [ | ||||
|   "confirm_error" => "Error confirming e-mail address", | ||||
|   "gpg_key" => "GPG Key", | ||||
|   "2fa_token" => "Two-Factor Authentication (2FA)", | ||||
|   "profile_picture_of" => "Profile Picture of", | ||||
| ]; | ||||
| @ -9,19 +9,23 @@ use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; | ||||
| class ApiKey extends DatabaseEntity { | ||||
| 
 | ||||
|   private bool $active; | ||||
|   #[MaxLength(64)] public String $apiKey;
 | ||||
|   #[MaxLength(64)] public String $token;
 | ||||
|   public \DateTime $validUntil; | ||||
|   public User $user; | ||||
| 
 | ||||
|   public function __construct(?int $id = null) { | ||||
|     parent::__construct($id); | ||||
|     $this->active = true; | ||||
|   } | ||||
| 
 | ||||
|   public function getValidUntil(): \DateTime { | ||||
|     return $this->validUntil; | ||||
|   } | ||||
| 
 | ||||
|   public static function create(User $user, int $days = 30): ApiKey { | ||||
|     $apiKey = new ApiKey(); | ||||
|     $apiKey->user = $user; | ||||
|     $apiKey->token = generateRandomString(64); | ||||
|     $apiKey->validUntil = (new \DateTime())->modify("+$days days"); | ||||
|     $apiKey->active = true; | ||||
|     return $apiKey; | ||||
|   } | ||||
| 
 | ||||
|   public function refresh(SQL $sql, int $days): bool { | ||||
|     $this->validUntil = (new \DateTime())->modify("+$days days"); | ||||
|     return $this->save($sql, ["validUntil"]); | ||||
|  | ||||
| @ -43,7 +43,7 @@ function uuidv4(): string { | ||||
|   return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); | ||||
| } | ||||
| 
 | ||||
| function generateRandomString($length, $type = "ascii"): string { | ||||
| function generateRandomString(int $length, $type = "ascii"): string { | ||||
|   $randomString = ''; | ||||
| 
 | ||||
|   $lowercase = "abcdefghijklmnopqrstuvwxyz"; | ||||
|  | ||||
| @ -156,8 +156,41 @@ export default class API { | ||||
|     } | ||||
| 
 | ||||
|     async updateProfile(username=null, fullName=null, password=null, confirmPassword = null, oldPassword = null) { | ||||
|         return this.apiCall("user/updateProfile", { username: username, fullName: fullName, | ||||
|         let res = await this.apiCall("user/updateProfile", { username: username, fullName: fullName, | ||||
|             password: password, confirmPassword: confirmPassword, oldPassword: oldPassword }); | ||||
| 
 | ||||
|         if (res.success) { | ||||
|             if (username !== null) { | ||||
|                 this.user.name = username; | ||||
|             } | ||||
| 
 | ||||
|             if (fullName !== null) { | ||||
|                 this.user.fullName = fullName; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async uploadPicture(file, scale=1.0) { | ||||
|         const formData = new FormData(); | ||||
|         formData.append("scale", scale); | ||||
|         formData.append("picture", file, file.name); | ||||
|         let res = await this.apiCall("user/uploadPicture", formData); | ||||
|         if (res.success) { | ||||
|             this.user.profilePicture = res.profilePicture; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async removePicture() { | ||||
|         let res = await this.apiCall("user/removePicture"); | ||||
|         if (res.success) { | ||||
|             this.user.profilePicture = null; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     /** Stats **/ | ||||
| @ -234,8 +267,8 @@ export default class API { | ||||
|     } | ||||
| 
 | ||||
|     /** ApiKeyAPI **/ | ||||
|     async getApiKeys(showActiveOnly = false) { | ||||
|         return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly }); | ||||
|     async getApiKeys(showActiveOnly = false, page = 1, count = 25, orderBy = "validUntil", sortOrder = "desc") { | ||||
|         return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly, pageNum: page, count: count, orderBy: orderBy, sortOrder: sortOrder }); | ||||
|     } | ||||
| 
 | ||||
|     async createApiKey() { | ||||
| @ -248,11 +281,21 @@ export default class API { | ||||
| 
 | ||||
|     /** 2FA API **/ | ||||
|     async confirmTOTP(code) { | ||||
|         return this.apiCall("tfa/confirmTotp", { code: code }); | ||||
|         let res = await this.apiCall("tfa/confirmTotp", { code: code }); | ||||
|         if (res.success) { | ||||
|             this.user.twoFactorToken = { type: "totp", confirmed: true }; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async remove2FA(password) { | ||||
|         return this.apiCall("tfa/remove", { password: password }); | ||||
|         let res = await this.apiCall("tfa/remove", { password: password }); | ||||
|         if (res.success) { | ||||
|             this.user.twoFactorToken = null; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async verifyTotp2FA(code) { | ||||
| @ -264,12 +307,22 @@ export default class API { | ||||
|     } | ||||
| 
 | ||||
|     async register2FA(clientDataJSON = null, attestationObject = null) { | ||||
|         return this.apiCall("tfa/registerKey", { clientDataJSON: clientDataJSON, attestationObject: attestationObject }); | ||||
|         let res = await this.apiCall("tfa/registerKey", { clientDataJSON: clientDataJSON, attestationObject: attestationObject }); | ||||
|         if (res.success && res.twoFactorToken) { | ||||
|             this.user.twoFactorToken = res.twoFactorToken; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     /** GPG API **/ | ||||
|     async uploadGPG(pubkey) { | ||||
|         return this.apiCall("user/importGPG", { pubkey: pubkey }); | ||||
|         let res = await this.apiCall("user/importGPG", { pubkey: pubkey }); | ||||
|         if (res.success) { | ||||
|             this.user.gpgKey = res.gpgKey; | ||||
|         } | ||||
| 
 | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async confirmGpgToken(token) { | ||||
|  | ||||
| @ -7,7 +7,6 @@ import {LocaleContext} from "../locale"; | ||||
| import clsx from "clsx"; | ||||
| import {Box, IconButton} from "@mui/material"; | ||||
| import {formatDateTime} from "../util"; | ||||
| import UserLink from "security-lab/src/elements/user/userlink"; | ||||
| import CachedIcon from "@material-ui/icons/Cached"; | ||||
| 
 | ||||
| 
 | ||||
| @ -137,6 +136,7 @@ export class DataColumn { | ||||
|         this.field = field; | ||||
|         this.sortable = !params.hasOwnProperty("sortable") || !!params.sortable; | ||||
|         this.align = params.align || "left"; | ||||
|         this.params = params; | ||||
|     } | ||||
| 
 | ||||
|     renderData(L, entry, index) { | ||||
| @ -152,6 +152,16 @@ export class StringColumn extends DataColumn { | ||||
|     constructor(label, field = null, params = {}) { | ||||
|         super(label, field, params); | ||||
|     } | ||||
| 
 | ||||
|     renderData(L, entry, index) { | ||||
|         let data = super.renderData(L, entry, index); | ||||
| 
 | ||||
|         if (this.params.style) { | ||||
|             data = <span style={this.params.style}>{data}</span> | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class NumericColumn extends DataColumn { | ||||
| @ -198,13 +208,14 @@ export class DateTimeColumn extends DataColumn { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class UserLinkColumn extends DataColumn { | ||||
| export class BoolColumn extends DataColumn { | ||||
|     constructor(label, field = null, params = {}) { | ||||
|         super(label, field, params); | ||||
|     } | ||||
| 
 | ||||
|     renderData(L, entry, index) { | ||||
|         return <UserLink user={super.renderData(L, entry)}/> | ||||
|         let data = super.renderData(L, entry); | ||||
|         return L(data ? "general.true" : "general.false"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import React from "react"; | ||||
| import clsx from "clsx"; | ||||
| import {Box, Modal} from "@mui/material"; | ||||
| import {Button, Typography} from "@material-ui/core"; | ||||
| import React, {useContext} from "react"; | ||||
| import {Dialog as MuiDialog,  DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; | ||||
| import {Button} from "@material-ui/core"; | ||||
| import {LocaleContext} from "../locale"; | ||||
| import "./dialog.css"; | ||||
| 
 | ||||
| export default function Dialog(props) { | ||||
| @ -11,6 +11,8 @@ export default function Dialog(props) { | ||||
|     const onOption = props.onOption || function() { }; | ||||
|     const options = props.options || ["Close"]; | ||||
|     const type = props.type || "default"; | ||||
|     const {translate: L} = useContext(LocaleContext); | ||||
| 
 | ||||
| 
 | ||||
|     let buttons = []; | ||||
|     for (let name of options) { | ||||
| @ -26,20 +28,19 @@ export default function Dialog(props) { | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     return <Modal | ||||
|     return <MuiDialog | ||||
|         open={show} | ||||
|         onClose={onClose} | ||||
|         aria-labelledby="modal-title" | ||||
|         aria-describedby="modal-description" | ||||
|     > | ||||
|         <Box className={clsx("modal-dialog", props.className)}> | ||||
|             <Typography id="modal-title" variant="h6" component="h2"> | ||||
|                 {props.title} | ||||
|             </Typography> | ||||
|             <Typography id="modal-description" sx={{ mt: 2 }}> | ||||
|                 {props.message} | ||||
|             </Typography> | ||||
|             { buttons } | ||||
|         </Box> | ||||
|     </Modal> | ||||
|         aria-labelledby="alert-dialog-title" | ||||
|         aria-describedby="alert-dialog-description"> | ||||
|         <DialogTitle>{ props.title }</DialogTitle> | ||||
|         <DialogContent> | ||||
|             <DialogContentText> | ||||
|                 { props.message } | ||||
|             </DialogContentText> | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|             {buttons} | ||||
|         </DialogActions> | ||||
|     </MuiDialog> | ||||
| } | ||||
| @ -185,7 +185,7 @@ export default function LoginForm(props) { | ||||
|         }).catch(e => { | ||||
|             set2FAToken({ ...tfaToken, step: 2, error: e.toString() }); | ||||
|         }); | ||||
|     }, [api.loggedIn, tfaToken, props.onLogin, abortSignal]); | ||||
|     }, [api.loggedIn, tfaToken, props.onLogin, props.onKey2FA, abortSignal]); | ||||
| 
 | ||||
|     const createForm = () => { | ||||
| 
 | ||||
| @ -335,7 +335,10 @@ export default function LoginForm(props) { | ||||
|     } | ||||
| 
 | ||||
|     if (!loaded) { | ||||
|         return <b>{L("general.loading")}… <Icon icon={"spinner"}/></b> | ||||
|         return <Box textAlign={"center"} mt={2}> | ||||
|             <h2>{L("general.loading", "Loading")}…</h2> | ||||
|             <CircularProgress size={"32px"}/> | ||||
|         </Box> | ||||
|     } | ||||
| 
 | ||||
|     let successMessage = getParameter("success"); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user