user settings
This commit is contained in:
parent
9e9bc5267a
commit
8eff66dcec
@ -9,6 +9,7 @@ import ProjectManager from './pages/ProjectManager';
|
|||||||
import SignIn from './pages/SignIn';
|
import SignIn from './pages/SignIn';
|
||||||
import ChatSessionView from './pages/ChatSessionView';
|
import ChatSessionView from './pages/ChatSessionView';
|
||||||
import DroneManager from './pages/DroneManager';
|
import DroneManager from './pages/DroneManager';
|
||||||
|
import Settings from './pages/Settings';
|
||||||
|
|
||||||
const TOKEN_KEY = 'dtp_auth_token';
|
const TOKEN_KEY = 'dtp_auth_token';
|
||||||
const USER_KEY = 'dtp_user';
|
const USER_KEY = 'dtp_user';
|
||||||
@ -122,6 +123,7 @@ export default function App() {
|
|||||||
<Route path="/projects/new" element={<ProjectManager user={user} />} />
|
<Route path="/projects/new" element={<ProjectManager user={user} />} />
|
||||||
<Route path="/projects/:slug" element={<ProjectManager user={user} />} />
|
<Route path="/projects/:slug" element={<ProjectManager user={user} />} />
|
||||||
<Route path="/drones" element={<DroneManager user={user} />} />
|
<Route path="/drones" element={<DroneManager user={user} />} />
|
||||||
|
<Route path="/settings" element={user ? <Settings /> : <Navigate to="/sign-in" replace />} />
|
||||||
<Route path="/projects/:projectId/chat-session/:sessionId" element={<ChatSessionView />} />
|
<Route path="/projects/:projectId/chat-session/:sessionId" element={<ChatSessionView />} />
|
||||||
<Route
|
<Route
|
||||||
path="/sign-in"
|
path="/sign-in"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import type { User } from '../lib/api';
|
import type { User } from '../lib/api';
|
||||||
|
|
||||||
const APP_VERSION = '1.0.0';
|
const APP_VERSION = '1.0.0';
|
||||||
@ -10,6 +10,7 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ user, onSignOut }: HeaderProps) {
|
export default function Header({ user, onSignOut }: HeaderProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -54,6 +55,10 @@ export default function Header({ user, onSignOut }: HeaderProps) {
|
|||||||
>
|
>
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<button
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
navigate('/settings');
|
||||||
|
}}
|
||||||
className="w-full px-4 py-2 text-left text-sm text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors"
|
className="w-full px-4 py-2 text-left text-sm text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
|
|||||||
@ -161,6 +161,7 @@ export interface User {
|
|||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
flags: string[];
|
flags: string[];
|
||||||
|
persona?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
@ -178,6 +179,15 @@ export interface Project {
|
|||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
updateSettings: (data: {
|
||||||
|
displayName?: string;
|
||||||
|
currentPassword?: string;
|
||||||
|
password?: string;
|
||||||
|
persona?: string;
|
||||||
|
}) => api.put<User>("/api/v1/user/settings", data),
|
||||||
|
};
|
||||||
|
|
||||||
export const projectApi = {
|
export const projectApi = {
|
||||||
getAll: () => api.get<Project[]>("/api/v1/projects"),
|
getAll: () => api.get<Project[]>("/api/v1/projects"),
|
||||||
get: (id: string) => api.get<Project>(`/api/v1/projects/${id}`),
|
get: (id: string) => api.get<Project>(`/api/v1/projects/${id}`),
|
||||||
|
|||||||
256
gadget-code/frontend/src/pages/Settings.tsx
Normal file
256
gadget-code/frontend/src/pages/Settings.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { userApi } from "../lib/api";
|
||||||
|
import { useAppContext } from "../App";
|
||||||
|
|
||||||
|
const PERSONA_MAX = 500;
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const { user, setStatusMessage } = useAppContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [displayName, setDisplayName] = useState(user?.displayName ?? "");
|
||||||
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [persona, setPersona] = useState(user?.persona ?? "");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [success, setSuccess] = useState("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const personaRemaining = PERSONA_MAX - persona.length;
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
|
||||||
|
if (displayName.length < 3 || displayName.length > 30) {
|
||||||
|
setError("Display Name must be between 3-30 characters.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changingPassword =
|
||||||
|
currentPassword || newPassword || confirmPassword;
|
||||||
|
|
||||||
|
if (changingPassword) {
|
||||||
|
if (!currentPassword) {
|
||||||
|
setError("Current password is required to change your password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newPassword) {
|
||||||
|
setError("New password is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
setError("New password must be at least 8 characters.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError("New passwords do not match.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persona.length > PERSONA_MAX) {
|
||||||
|
setError(`Persona must be ${PERSONA_MAX} characters or fewer.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const body: Record<string, string | undefined> = {
|
||||||
|
displayName,
|
||||||
|
persona,
|
||||||
|
};
|
||||||
|
if (changingPassword) {
|
||||||
|
body.currentPassword = currentPassword;
|
||||||
|
body.password = newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await userApi.updateSettings(body);
|
||||||
|
setSuccess("Settings saved successfully.");
|
||||||
|
setStatusMessage("Settings updated.");
|
||||||
|
|
||||||
|
setCurrentPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
|
||||||
|
const storedUser = localStorage.getItem("dtp_user");
|
||||||
|
if (storedUser) {
|
||||||
|
const parsed = JSON.parse(storedUser);
|
||||||
|
Object.assign(parsed, updatedUser);
|
||||||
|
localStorage.setItem("dtp_user", JSON.stringify(parsed));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to save settings.");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex bg-bg-primary overflow-hidden">
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
<div className="max-w-xl mx-auto">
|
||||||
|
<div className="border-2 border-border-default p-6 rounded bg-bg-secondary">
|
||||||
|
<div className="font-mono text-text-secondary text-sm">
|
||||||
|
<div className="mb-1 text-text-muted">// USER SETTINGS</div>
|
||||||
|
<div className="mb-6 text-text-primary">ACCOUNT CONFIGURATION</div>
|
||||||
|
|
||||||
|
<div className="mb-6 p-3 border border-border-subtle rounded bg-bg-tertiary">
|
||||||
|
<div className="text-text-muted text-xs">
|
||||||
|
Signed in as <span className="text-text-primary">{user?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-text-muted text-xs mt-1">
|
||||||
|
Email address cannot be changed. Contact your administrator.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 border border-red-800 rounded bg-red-950/50">
|
||||||
|
<div className="text-red-400 font-mono text-sm">
|
||||||
|
<span className="text-text-muted">// ERROR: </span>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="mb-4 p-3 border border-green-800 rounded bg-green-950/50">
|
||||||
|
<div className="text-green-400 font-mono text-sm">
|
||||||
|
<span className="text-text-muted">// SUCCESS: </span>
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="displayName"
|
||||||
|
className="block text-sm text-text-muted mb-1 font-mono"
|
||||||
|
>
|
||||||
|
display_name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="displayName"
|
||||||
|
type="text"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
|
||||||
|
minLength={3}
|
||||||
|
maxLength={30}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted mb-3 font-mono tracking-wider">
|
||||||
|
--- PASSWORD ---
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="currentPassword"
|
||||||
|
className="block text-sm text-text-muted mb-1 font-mono"
|
||||||
|
>
|
||||||
|
current_password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="currentPassword"
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="newPassword"
|
||||||
|
className="block text-sm text-text-muted mb-1 font-mono"
|
||||||
|
>
|
||||||
|
new_password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="newPassword"
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm text-text-muted mb-1 font-mono"
|
||||||
|
>
|
||||||
|
confirm_new_password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-text-muted mb-3 font-mono tracking-wider">
|
||||||
|
--- PERSONA ---
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
htmlFor="persona"
|
||||||
|
className="block text-sm text-text-muted mb-1 font-mono"
|
||||||
|
>
|
||||||
|
Describe yourself for your Agent
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="persona"
|
||||||
|
value={persona}
|
||||||
|
onChange={(e) => setPersona(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary font-mono focus:outline-none focus:border-border-highlight resize-y min-h-[120px]"
|
||||||
|
maxLength={PERSONA_MAX}
|
||||||
|
placeholder="e.g. I'm a frontend engineer focused on React. I need extra help with accessibility and testing..."
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`text-right text-xs mt-1 font-mono ${
|
||||||
|
personaRemaining < 50
|
||||||
|
? "text-red-400"
|
||||||
|
: "text-text-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{personaRemaining}/{PERSONA_MAX} characters remaining
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
className="flex-1 px-4 py-2 text-center border border-border-highlight text-text-primary hover:bg-bg-tertiary rounded transition-colors font-mono text-sm"
|
||||||
|
>
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="flex-1 px-4 py-2 bg-brand hover:bg-red-700 text-white rounded transition-colors disabled:opacity-50 font-mono text-sm"
|
||||||
|
>
|
||||||
|
{saving ? "SAVING..." : "SAVE SETTINGS"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,9 +2,10 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
|
||||||
import { DtpController } from "../../../lib/controller.js";
|
import { DtpController } from "../../../lib/controller.js";
|
||||||
|
import UserService from "../../../services/user.js";
|
||||||
|
|
||||||
export class UserApiControllerV1 extends DtpController {
|
export class UserApiControllerV1 extends DtpController {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
@ -25,6 +26,7 @@ export class UserApiControllerV1 extends DtpController {
|
|||||||
this.router.use(this.requireUser());
|
this.router.use(this.requireUser());
|
||||||
|
|
||||||
this.router.get("/", this.getUser.bind(this));
|
this.router.get("/", this.getUser.bind(this));
|
||||||
|
this.router.put("/settings", this.putUpdateSettings.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(req: Request, res: Response): Promise<void> {
|
async getUser(req: Request, res: Response): Promise<void> {
|
||||||
@ -35,6 +37,25 @@ export class UserApiControllerV1 extends DtpController {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async putUpdateSettings(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const updatedUser = await UserService.updateForUser(req.user!, req.body);
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: updatedUser,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to update user settings", { error });
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserApiControllerV1;
|
export default UserApiControllerV1;
|
||||||
|
|||||||
@ -18,9 +18,10 @@ import { DtpService } from "../lib/service.js";
|
|||||||
import { DtpPaginationParameters } from "../lib/pagination-parameters.ts";
|
import { DtpPaginationParameters } from "../lib/pagination-parameters.ts";
|
||||||
|
|
||||||
export interface IUserUpdate {
|
export interface IUserUpdate {
|
||||||
email: string;
|
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
currentPassword?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
persona?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserAdminUpdate {
|
export interface IUserAdminUpdate {
|
||||||
@ -127,29 +128,79 @@ class UserService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateForUser(user: IUser, def: IUserUpdate): Promise<IUser> {
|
async updateForUser(user: IUser, def: IUserUpdate): Promise<IUser> {
|
||||||
const update: MongooseBaseQueryOptions = { $set: {} };
|
const update: Record<string, unknown> = { $set: {} };
|
||||||
let needsEmailVerification = false;
|
const $set = update.$set as Record<string, unknown>;
|
||||||
|
|
||||||
if (def.email && def.email !== user.email) {
|
if (def.displayName !== undefined) {
|
||||||
update.$set.email = filterText(def.email);
|
const displayName = filterText(def.displayName);
|
||||||
update.$set.email_lc = update.$set.email.toLowerCase();
|
if (displayName.length < 3 || displayName.length > 30) {
|
||||||
update.$set["flags.isEmailVerified"] = false;
|
const err = new Error(
|
||||||
needsEmailVerification = true;
|
"Display Name must be between 3-30 characters in length.",
|
||||||
this.log.info("updating user email address", {
|
);
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
$set.displayName = displayName;
|
||||||
|
this.log.info("updating user display name", {
|
||||||
|
user: { _id: user._id },
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.persona !== undefined) {
|
||||||
|
if (def.persona.length > 500) {
|
||||||
|
const err = new Error(
|
||||||
|
"Persona must be 500 characters or fewer.",
|
||||||
|
);
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
$set.persona = def.persona;
|
||||||
|
this.log.info("updating user persona", {
|
||||||
user: { _id: user._id },
|
user: { _id: user._id },
|
||||||
email: update.$set.email,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (def.password) {
|
if (def.password) {
|
||||||
|
if (!def.currentPassword) {
|
||||||
|
const err = new Error(
|
||||||
|
"Current password is required to set a new password.",
|
||||||
|
);
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ _id: user._id }).select(
|
||||||
|
"+passwordSalt +password",
|
||||||
|
);
|
||||||
|
if (!existingUser) {
|
||||||
|
const error = new Error(`User ${user._id} not found`);
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskedCurrent = await CryptoService.maskPassword(
|
||||||
|
existingUser.passwordSalt!,
|
||||||
|
def.currentPassword.trim(),
|
||||||
|
);
|
||||||
|
if (maskedCurrent !== existingUser.password) {
|
||||||
|
const err = new Error("Current password is incorrect.");
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.info("updating user password", { user: { _id: user._id } });
|
this.log.info("updating user password", { user: { _id: user._id } });
|
||||||
update.$set.passwordSalt = uuidv4();
|
$set.passwordSalt = uuidv4();
|
||||||
update.$set.password = await CryptoService.maskPassword(
|
$set.password = await CryptoService.maskPassword(
|
||||||
update.$set.passwordSalt,
|
$set.passwordSalt as string,
|
||||||
def.password.trim(),
|
def.password.trim(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.keys($set).length === 0) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
const newUser = await User.findOneAndUpdate({ _id: user._id }, update, {
|
const newUser = await User.findOneAndUpdate({ _id: user._id }, update, {
|
||||||
new: true,
|
new: true,
|
||||||
});
|
});
|
||||||
@ -159,10 +210,6 @@ class UserService extends DtpService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsEmailVerification) {
|
|
||||||
await ContactService.sendVerificationEmail(newUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user