mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-18 21:19:44 +00:00
feat: add member list in setting
This commit is contained in:
parent
c00f7d0852
commit
5db3506cba
@ -56,7 +56,7 @@ type CreateUserRequest struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Role Role `json:"-"`
|
Role Role `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (create CreateUserRequest) Validate() error {
|
func (create CreateUserRequest) Validate() error {
|
||||||
@ -78,9 +78,56 @@ type PatchUserRequest struct {
|
|||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
Nickname *string `json:"nickname"`
|
Nickname *string `json:"nickname"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
|
Role *Role `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
||||||
|
g.POST("/user", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||||
|
}
|
||||||
|
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
|
||||||
|
}
|
||||||
|
if currentUser == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||||
|
}
|
||||||
|
if currentUser.Role != store.RoleAdmin {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
|
||||||
|
}
|
||||||
|
|
||||||
|
userCreate := &CreateUserRequest{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
|
||||||
|
}
|
||||||
|
if err := userCreate.Validate(); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.CreateUser(ctx, &store.User{
|
||||||
|
Role: store.Role(userCreate.Role),
|
||||||
|
Email: userCreate.Email,
|
||||||
|
Nickname: userCreate.Nickname,
|
||||||
|
PasswordHash: string(passwordHash),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userMessage := convertUserFromStore(user)
|
||||||
|
return c.JSON(http.StatusOK, userMessage)
|
||||||
|
})
|
||||||
|
|
||||||
g.GET("/user", func(c echo.Context) error {
|
g.GET("/user", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
list, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
list, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||||
@ -140,7 +187,16 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||||
}
|
}
|
||||||
if currentUserID != userID {
|
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: ¤tUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to find current user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if currentUser == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||||
|
}
|
||||||
|
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, "access forbidden for current session user").SetInternal(err)
|
return echo.NewHTTPError(http.StatusForbidden, "access forbidden for current session user").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +206,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUser := &store.UpdateUser{
|
updateUser := &store.UpdateUser{
|
||||||
ID: currentUserID,
|
ID: userID,
|
||||||
}
|
}
|
||||||
if userPatch.Email != nil {
|
if userPatch.Email != nil {
|
||||||
if !validateEmail(*userPatch.Email) {
|
if !validateEmail(*userPatch.Email) {
|
||||||
@ -170,6 +226,14 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
|||||||
passwordHashStr := string(passwordHash)
|
passwordHashStr := string(passwordHash)
|
||||||
updateUser.PasswordHash = &passwordHashStr
|
updateUser.PasswordHash = &passwordHashStr
|
||||||
}
|
}
|
||||||
|
if userPatch.RowStatus != nil {
|
||||||
|
rowStatus := store.RowStatus(*userPatch.RowStatus)
|
||||||
|
updateUser.RowStatus = &rowStatus
|
||||||
|
}
|
||||||
|
if userPatch.Role != nil {
|
||||||
|
role := store.Role(*userPatch.Role)
|
||||||
|
updateUser.Role = &role
|
||||||
|
}
|
||||||
|
|
||||||
user, err := s.Store.UpdateUser(ctx, updateUser)
|
user, err := s.Store.UpdateUser(ctx, updateUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -202,6 +266,18 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user id is not a number: %s", c.Param("id"))).SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||||
}
|
}
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user, err: %s", err)).SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user not found with ID: %d", userID)).SetInternal(err)
|
||||||
|
}
|
||||||
|
if user.Role == store.RoleAdmin {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("cannot delete admin user with ID: %d", userID)).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
|
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
|
||||||
ID: userID,
|
ID: userID,
|
||||||
|
@ -1,15 +1,33 @@
|
|||||||
import { CssVarsProvider } from "@mui/joy/styles";
|
import { useEffect, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Outlet } from "react-router-dom";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { globalService } from "./services";
|
||||||
import router from "./routers";
|
import useUserStore from "./stores/v1/user";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
const userStore = useUserStore();
|
||||||
<CssVarsProvider>
|
const [loading, setLoading] = useState(true);
|
||||||
<RouterProvider router={router} />
|
|
||||||
<Toaster position="top-center" />
|
useEffect(() => {
|
||||||
</CssVarsProvider>
|
const initialState = async () => {
|
||||||
);
|
try {
|
||||||
|
await globalService.initialState();
|
||||||
|
} catch (error) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userStore.fetchCurrentUser();
|
||||||
|
} catch (error) {
|
||||||
|
// do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
initialState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{!loading && <Outlet />}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -2,7 +2,7 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { userService } from "../services";
|
import useUserStore from "../stores/v1/user";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -11,6 +11,7 @@ interface Props {
|
|||||||
|
|
||||||
const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
|
const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
|
const userStore = useUserStore();
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
||||||
const requestState = useLoading(false);
|
const requestState = useLoading(false);
|
||||||
@ -43,9 +44,8 @@ const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
requestState.setLoading();
|
requestState.setLoading();
|
||||||
try {
|
try {
|
||||||
const user = userService.getState().user as User;
|
userStore.patchUser({
|
||||||
await userService.patchUser({
|
id: userStore.getCurrentUser().id,
|
||||||
id: user.id,
|
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
201
web/src/components/CreateUserDialog.tsx
Normal file
201
web/src/components/CreateUserDialog.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import useLoading from "../hooks/useLoading";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user?: User;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
userCreate: UserCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles: Role[] = ["USER", "ADMIN"];
|
||||||
|
|
||||||
|
const CreateUserDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { onClose, onConfirm, user } = props;
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
userCreate: {
|
||||||
|
email: "",
|
||||||
|
nickname: "",
|
||||||
|
password: "",
|
||||||
|
role: "USER",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const requestState = useLoading(false);
|
||||||
|
const isEditing = !!user;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
userCreate: Object.assign(state.userCreate, {
|
||||||
|
email: user.email,
|
||||||
|
nickname: user.nickname,
|
||||||
|
password: "",
|
||||||
|
role: user.role,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const setPartialState = (partialState: Partial<State>) => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
...partialState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPartialState({
|
||||||
|
userCreate: Object.assign(state.userCreate, {
|
||||||
|
email: e.target.value.toLowerCase(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNicknameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPartialState({
|
||||||
|
userCreate: Object.assign(state.userCreate, {
|
||||||
|
nickname: e.target.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPartialState({
|
||||||
|
userCreate: Object.assign(state.userCreate, {
|
||||||
|
password: e.target.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPartialState({
|
||||||
|
userCreate: Object.assign(state.userCreate, {
|
||||||
|
visibility: e.target.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (!state.userCreate.email) {
|
||||||
|
toast.error("Email is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (user) {
|
||||||
|
const userPatch: UserPatch = {
|
||||||
|
id: user.id,
|
||||||
|
};
|
||||||
|
if (user.email !== state.userCreate.email) {
|
||||||
|
userPatch.email = state.userCreate.email;
|
||||||
|
}
|
||||||
|
if (user.nickname !== state.userCreate.nickname) {
|
||||||
|
userPatch.nickname = state.userCreate.nickname;
|
||||||
|
}
|
||||||
|
if (state.userCreate.password) {
|
||||||
|
userPatch.password = state.userCreate.password;
|
||||||
|
}
|
||||||
|
if (user.role !== state.userCreate.role) {
|
||||||
|
userPatch.role = state.userCreate.role;
|
||||||
|
}
|
||||||
|
await userStore.patchUser(userPatch);
|
||||||
|
} else {
|
||||||
|
await userStore.createUser(state.userCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.response.data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={true}>
|
||||||
|
<ModalDialog>
|
||||||
|
<div className="flex flex-row justify-between items-center w-80 sm:w-96 mb-4">
|
||||||
|
<span className="text-lg font-medium">{isEditing ? "Edit User" : "Create User"}</span>
|
||||||
|
<Button variant="plain" onClick={onClose}>
|
||||||
|
<Icon.X className="w-5 h-auto text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
|
<span className="mb-2">
|
||||||
|
Email <span className="text-red-600">*</span>
|
||||||
|
</span>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
type="email"
|
||||||
|
placeholder="Unique user email"
|
||||||
|
value={state.userCreate.email}
|
||||||
|
onChange={handleEmailInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
|
<span className="mb-2">
|
||||||
|
Nickname <span className="text-red-600">*</span>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nickname"
|
||||||
|
value={state.userCreate.nickname}
|
||||||
|
onChange={handleNicknameInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
|
<span className="mb-2">
|
||||||
|
Password
|
||||||
|
{!isEditing && <span className="text-red-600"> *</span>}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
type="password"
|
||||||
|
placeholder=""
|
||||||
|
value={state.userCreate.password}
|
||||||
|
onChange={handlePasswordInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
|
<span className="mb-2">
|
||||||
|
Role <span className="text-red-600">*</span>
|
||||||
|
</span>
|
||||||
|
<div className="w-full flex flex-row justify-start items-center text-base">
|
||||||
|
<RadioGroup orientation="horizontal" value={state.userCreate.role} onChange={handleRoleInputChange}>
|
||||||
|
{roles.map((role) => (
|
||||||
|
<Radio key={role} value={role} label={role} />
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
|
||||||
|
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUserDialog;
|
@ -2,8 +2,7 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { userService } from "../services";
|
import useUserStore from "../stores/v1/user";
|
||||||
import { useAppSelector } from "../stores";
|
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -12,9 +11,10 @@ interface Props {
|
|||||||
|
|
||||||
const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
|
const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
const user = useAppSelector((state) => state.user.user as User);
|
const userStore = useUserStore();
|
||||||
const [email, setEmail] = useState(user.email);
|
const currentUser = userStore.getCurrentUser();
|
||||||
const [nickname, setNickname] = useState(user.nickname);
|
const [email, setEmail] = useState(currentUser.email);
|
||||||
|
const [nickname, setNickname] = useState(currentUser.nickname);
|
||||||
const requestState = useLoading(false);
|
const requestState = useLoading(false);
|
||||||
|
|
||||||
const handleCloseBtnClick = () => {
|
const handleCloseBtnClick = () => {
|
||||||
@ -39,14 +39,13 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
requestState.setLoading();
|
requestState.setLoading();
|
||||||
try {
|
try {
|
||||||
const user = userService.getState().user as User;
|
await userStore.patchUser({
|
||||||
await userService.patchUser({
|
id: currentUser.id,
|
||||||
id: user.id,
|
|
||||||
email,
|
email,
|
||||||
nickname,
|
nickname,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
toast("Password changed");
|
toast("User information updated");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.response.data.message);
|
toast.error(error.response.data.message);
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Avatar } from "@mui/joy";
|
import { Avatar } from "@mui/joy";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAppSelector } from "../stores";
|
import useUserStore from "../stores/v1/user";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import AboutDialog from "./AboutDialog";
|
import AboutDialog from "./AboutDialog";
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useAppSelector((state) => state.user).user as User;
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const [showAboutDialog, setShowAboutDialog] = useState<boolean>(false);
|
const [showAboutDialog, setShowAboutDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSignOutButtonClick = async () => {
|
const handleSignOutButtonClick = async () => {
|
||||||
@ -30,7 +30,7 @@ const Header: React.FC = () => {
|
|||||||
trigger={
|
trigger={
|
||||||
<button className="flex flex-row justify-end items-center cursor-pointer">
|
<button className="flex flex-row justify-end items-center cursor-pointer">
|
||||||
<Avatar size="sm" variant="plain" />
|
<Avatar size="sm" variant="plain" />
|
||||||
<span>{user.nickname}</span>
|
<span>{currentUser.nickname}</span>
|
||||||
<Icon.ChevronDown className="ml-2 w-5 h-auto text-gray-600" />
|
<Icon.ChevronDown className="ml-2 w-5 h-auto text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { shortcutService } from "../services";
|
import { shortcutService } from "../services";
|
||||||
import { useAppSelector } from "../stores";
|
|
||||||
import useFaviconStore from "../stores/v1/favicon";
|
import useFaviconStore from "../stores/v1/favicon";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
import { absolutifyLink } from "../helpers/utils";
|
import { absolutifyLink } from "../helpers/utils";
|
||||||
import { showCommonDialog } from "./Alert";
|
import { showCommonDialog } from "./Alert";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -21,11 +21,11 @@ interface Props {
|
|||||||
const ShortcutView = (props: Props) => {
|
const ShortcutView = (props: Props) => {
|
||||||
const { shortcut, handleEdit } = props;
|
const { shortcut, handleEdit } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const user = useAppSelector((state) => state.user.user as User);
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const faviconStore = useFaviconStore();
|
const faviconStore = useFaviconStore();
|
||||||
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||||
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
|
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
|
||||||
const havePermission = user.role === "ADMIN" || shortcut.creatorId === user.id;
|
const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id;
|
||||||
const shortifyLink = absolutifyLink(`/s/${shortcut.name}`);
|
const shortifyLink = absolutifyLink(`/s/${shortcut.name}`);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppSelector } from "../../stores";
|
import useUserStore from "../../stores/v1/user";
|
||||||
import ChangePasswordDialog from "../ChangePasswordDialog";
|
import ChangePasswordDialog from "../ChangePasswordDialog";
|
||||||
import EditUserinfoDialog from "../EditUserinfoDialog";
|
import EditUserinfoDialog from "../EditUserinfoDialog";
|
||||||
|
|
||||||
const AccountSection: React.FC = () => {
|
const AccountSection: React.FC = () => {
|
||||||
const user = useAppSelector((state) => state.user).user as User;
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState<boolean>(false);
|
const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState<boolean>(false);
|
||||||
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState<boolean>(false);
|
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState<boolean>(false);
|
||||||
const isAdmin = user.role === "ADMIN";
|
const isAdmin = currentUser.role === "ADMIN";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start gap-y-2">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start gap-y-2">
|
||||||
<p className="text-gray-400">Account</p>
|
<p className="text-base font-semibold leading-6 text-gray-900">Account</p>
|
||||||
<p className="flex flex-row justify-start items-center mt-2">
|
<p className="flex flex-row justify-start items-center mt-2">
|
||||||
<span className="text-xl font-medium">{user.nickname}</span>
|
<span className="text-xl">{currentUser.nickname}</span>
|
||||||
{isAdmin && <span className="ml-2 bg-blue-600 text-white px-2 leading-6 text-sm rounded-full">Admin</span>}
|
{isAdmin && <span className="ml-2 bg-blue-600 text-white px-2 leading-6 text-sm rounded-full">Admin</span>}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex flex-row justify-start items-center">
|
<p className="flex flex-row justify-start items-center">
|
||||||
<span className="mr-3 text-gray-500 font-mono">Email: </span>
|
<span className="mr-3 text-gray-500 font-mono">Email: </span>
|
||||||
{user.email}
|
{currentUser.email}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row justify-start items-center gap-2 mt-2">
|
<div className="flex flex-row justify-start items-center gap-2 mt-2">
|
||||||
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>
|
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>
|
||||||
|
95
web/src/components/setting/UserSection.tsx
Normal file
95
web/src/components/setting/UserSection.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@mui/joy";
|
||||||
|
import CreateUserDialog from "../CreateUserDialog";
|
||||||
|
import useUserStore from "../../stores/v1/user";
|
||||||
|
|
||||||
|
const MemberSection = () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const [showCreateUserDialog, setShowCreateUserDialog] = useState<boolean>(false);
|
||||||
|
const [currentEditingUser, setCurrentEditingUser] = useState<User | undefined>(undefined);
|
||||||
|
const userList = Object.values(userStore.userMap);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
userStore.fetchUserList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateUserDialogClose = () => {
|
||||||
|
setShowCreateUserDialog(false);
|
||||||
|
setCurrentEditingUser(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="sm:flex sm:items-center">
|
||||||
|
<div className="sm:flex-auto">
|
||||||
|
<p className="text-base font-semibold leading-6 text-gray-900">Users</p>
|
||||||
|
<p className="mt-2 text-sm text-gray-700">
|
||||||
|
A list of all the users in your workspace including their nickname, email and role.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateUserDialog(true);
|
||||||
|
setCurrentEditingUser(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add user
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flow-root">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900">
|
||||||
|
Nickname
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative py-3.5 pl-3 pr-4">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{userList.map((user) => (
|
||||||
|
<tr key={user.email}>
|
||||||
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900">{user.nickname}</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.email}</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
|
||||||
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium">
|
||||||
|
<button
|
||||||
|
className="text-indigo-600 hover:text-indigo-900"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentEditingUser(user);
|
||||||
|
setShowCreateUserDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreateUserDialog && <CreateUserDialog user={currentEditingUser} onClose={handleCreateUserDialogClose} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberSection;
|
@ -18,10 +18,9 @@ const WorkspaceSection: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
||||||
<p className="text-gray-400">Workspace settings</p>
|
<p className="text-base font-semibold leading-6 text-gray-900">Workspace settings</p>
|
||||||
<div className="w-full flex flex-col justify-start items-start">
|
<div className="w-full flex flex-col justify-start items-start">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="font-medium"
|
|
||||||
label="Disable user signup"
|
label="Disable user signup"
|
||||||
checked={disallowSignUp}
|
checked={disallowSignUp}
|
||||||
onChange={(event) => handleDisallowSignUpChange(event.target.checked)}
|
onChange={(event) => handleDisallowSignUpChange(event.target.checked)}
|
||||||
|
@ -35,6 +35,10 @@ export function getUserById(id: number) {
|
|||||||
return axios.get<User>(`/api/v1/user/${id}`);
|
return axios.get<User>(`/api/v1/user/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createUser(userCreate: UserCreate) {
|
||||||
|
return axios.post<User>("/api/v1/user", userCreate);
|
||||||
|
}
|
||||||
|
|
||||||
export function patchUser(userPatch: UserPatch) {
|
export function patchUser(userPatch: UserPatch) {
|
||||||
return axios.patch<User>(`/api/v1/user/${userPatch.id}`, userPatch);
|
return axios.patch<User>(`/api/v1/user/${userPatch.id}`, userPatch);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { useEffect } from "react";
|
||||||
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
|
|
||||||
const Root: React.FC = () => {
|
const Root: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser) {
|
||||||
|
navigate("/auth", {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-start items-start">
|
<>
|
||||||
<Header />
|
{currentUser && (
|
||||||
<Outlet />
|
<div className="w-full h-full flex flex-col justify-start items-start">
|
||||||
</div>
|
<Header />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
|
import { RouterProvider } from "react-router-dom";
|
||||||
import store from "./stores";
|
import store from "./stores";
|
||||||
import App from "./App";
|
import router from "./routers";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import "./css/index.css";
|
import "./css/index.css";
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
const root = createRoot(container as HTMLElement);
|
const root = createRoot(container as HTMLElement);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<CssVarsProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
<Toaster position="top-center" />
|
||||||
|
</CssVarsProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppSelector } from "../stores";
|
import useUserStore from "../stores/v1/user";
|
||||||
import ChangePasswordDialog from "../components/ChangePasswordDialog";
|
import ChangePasswordDialog from "../components/ChangePasswordDialog";
|
||||||
import EditUserinfoDialog from "../components/EditUserinfoDialog";
|
import EditUserinfoDialog from "../components/EditUserinfoDialog";
|
||||||
|
|
||||||
const Account: React.FC = () => {
|
const Account: React.FC = () => {
|
||||||
const user = useAppSelector((state) => state.user).user as User;
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState<boolean>(false);
|
const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState<boolean>(false);
|
||||||
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState<boolean>(false);
|
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
||||||
<p className="text-3xl my-2">{user.nickname}</p>
|
<p className="text-3xl my-2">{currentUser.nickname}</p>
|
||||||
<p className="leading-8 flex flex-row justify-start items-center">
|
<p className="leading-8 flex flex-row justify-start items-center">
|
||||||
<span className="mr-3 text-gray-500 font-mono">Email: </span>
|
<span className="mr-3 text-gray-500 font-mono">Email: </span>
|
||||||
{user.email}
|
{currentUser.email}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row justify-start items-center gap-2">
|
<div className="flex flex-row justify-start items-center gap-2">
|
||||||
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>
|
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>
|
||||||
|
@ -2,6 +2,7 @@ import { Button, Tab, TabList, Tabs } from "@mui/joy";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { shortcutService } from "../services";
|
import { shortcutService } from "../services";
|
||||||
import { useAppSelector } from "../stores";
|
import { useAppSelector } from "../stores";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import ShortcutListView from "../components/ShortcutListView";
|
import ShortcutListView from "../components/ShortcutListView";
|
||||||
@ -13,13 +14,14 @@ interface State {
|
|||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||||
const user = useAppSelector((state) => state.user).user as User;
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
showCreateShortcutDialog: false,
|
showCreateShortcutDialog: false,
|
||||||
});
|
});
|
||||||
const [selectedFilter, setSelectFilter] = useState<"ALL" | "PRIVATE">("ALL");
|
const [selectedFilter, setSelectFilter] = useState<"ALL" | "PRIVATE">("ALL");
|
||||||
const filteredShortcutList = selectedFilter === "ALL" ? shortcutList : shortcutList.filter((shortcut) => shortcut.creatorId === user.id);
|
const filteredShortcutList =
|
||||||
|
selectedFilter === "ALL" ? shortcutList : shortcutList.filter((shortcut) => shortcut.creatorId === currentUser.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([shortcutService.getMyAllShortcuts()]).finally(() => {
|
Promise.all([shortcutService.getMyAllShortcuts()]).finally(() => {
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { useAppSelector } from "../stores";
|
import useUserStore from "../stores/v1/user";
|
||||||
import AccountSection from "../components/setting/AccountSection";
|
import AccountSection from "../components/setting/AccountSection";
|
||||||
import WorkspaceSection from "../components/setting/WorkspaceSection";
|
import WorkspaceSection from "../components/setting/WorkspaceSection";
|
||||||
|
import UserSection from "../components/setting/UserSection";
|
||||||
|
|
||||||
const Setting: React.FC = () => {
|
const Setting: React.FC = () => {
|
||||||
const user = useAppSelector((state) => state.user).user as User;
|
const currentUser = useUserStore().getCurrentUser();
|
||||||
const isAdmin = user.role === "ADMIN";
|
const isAdmin = currentUser.role === "ADMIN";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
|
||||||
<AccountSection />
|
<AccountSection />
|
||||||
{isAdmin && <WorkspaceSection />}
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<UserSection />
|
||||||
|
<WorkspaceSection />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,12 +3,13 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
import { userService } from "../services";
|
|
||||||
import { useAppSelector } from "../stores";
|
import { useAppSelector } from "../stores";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
|
|
||||||
const SignIn: React.FC = () => {
|
const SignIn: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const userStore = useUserStore();
|
||||||
const {
|
const {
|
||||||
workspaceProfile: { disallowSignUp },
|
workspaceProfile: { disallowSignUp },
|
||||||
} = useAppSelector((state) => state.global);
|
} = useAppSelector((state) => state.global);
|
||||||
@ -18,7 +19,7 @@ const SignIn: React.FC = () => {
|
|||||||
const allowConfirm = email.length > 0 && password.length > 0;
|
const allowConfirm = email.length > 0 && password.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userService.doSignOut();
|
api.signout();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -39,7 +40,7 @@ const SignIn: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
actionBtnLoadingState.setLoading();
|
actionBtnLoadingState.setLoading();
|
||||||
await api.signin(email, password);
|
await api.signin(email, password);
|
||||||
const user = await userService.doSignIn();
|
const user = await userStore.fetchCurrentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/", {
|
navigate("/", {
|
||||||
replace: true,
|
replace: true,
|
||||||
|
@ -3,11 +3,16 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
import { userService } from "../services";
|
import { globalService } from "../services";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
|
import useUserStore from "../stores/v1/user";
|
||||||
|
|
||||||
const SignUp: React.FC = () => {
|
const SignUp: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const {
|
||||||
|
workspaceProfile: { disallowSignUp },
|
||||||
|
} = globalService.getState();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [nickname, setNickname] = useState("");
|
const [nickname, setNickname] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@ -15,7 +20,15 @@ const SignUp: React.FC = () => {
|
|||||||
const allowConfirm = email.length > 0 && nickname.length > 0 && password.length > 0;
|
const allowConfirm = email.length > 0 && nickname.length > 0 && password.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userService.doSignOut();
|
if (disallowSignUp) {
|
||||||
|
return navigate("/auth", {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.signout();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -41,7 +54,7 @@ const SignUp: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
actionBtnLoadingState.setLoading();
|
actionBtnLoadingState.setLoading();
|
||||||
await api.signup(email, nickname, password);
|
await api.signup(email, nickname, password);
|
||||||
const user = await userService.doSignIn();
|
const user = await userStore.fetchCurrentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/", {
|
navigate("/", {
|
||||||
replace: true,
|
replace: true,
|
||||||
|
@ -1,83 +1,37 @@
|
|||||||
import { createBrowserRouter, redirect } from "react-router-dom";
|
import { createBrowserRouter } from "react-router-dom";
|
||||||
import { isNullorUndefined } from "../helpers/utils";
|
|
||||||
import { globalService, userService } from "../services";
|
|
||||||
import Root from "../layouts/Root";
|
import Root from "../layouts/Root";
|
||||||
import SignIn from "../pages/SignIn";
|
import SignIn from "../pages/SignIn";
|
||||||
import SignUp from "../pages/SignUp";
|
import SignUp from "../pages/SignUp";
|
||||||
import Home from "../pages/Home";
|
import Home from "../pages/Home";
|
||||||
import Setting from "../pages/Setting";
|
import Setting from "../pages/Setting";
|
||||||
|
import App from "../App";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
|
||||||
path: "/auth",
|
|
||||||
element: <SignIn />,
|
|
||||||
loader: async () => {
|
|
||||||
try {
|
|
||||||
await globalService.initialState();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/auth/signup",
|
|
||||||
element: <SignUp />,
|
|
||||||
loader: async () => {
|
|
||||||
try {
|
|
||||||
await globalService.initialState();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
workspaceProfile: { disallowSignUp },
|
|
||||||
} = globalService.getState();
|
|
||||||
if (disallowSignUp) {
|
|
||||||
return redirect("/auth");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <Root />,
|
element: <App />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "auth",
|
||||||
element: <Home />,
|
element: <SignIn />,
|
||||||
loader: async () => {
|
|
||||||
try {
|
|
||||||
await userService.initialState();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user } = userService.getState();
|
|
||||||
if (isNullorUndefined(user)) {
|
|
||||||
return redirect("/auth");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/setting",
|
path: "auth/signup",
|
||||||
element: <Setting />,
|
element: <SignUp />,
|
||||||
loader: async () => {
|
},
|
||||||
try {
|
{
|
||||||
await userService.initialState();
|
path: "",
|
||||||
} catch (error) {
|
element: <Root />,
|
||||||
// do nth
|
children: [
|
||||||
}
|
{
|
||||||
|
path: "",
|
||||||
const { user } = userService.getState();
|
element: <Home />,
|
||||||
if (isNullorUndefined(user)) {
|
},
|
||||||
return redirect("/auth");
|
{
|
||||||
}
|
path: "/setting",
|
||||||
return null;
|
element: <Setting />,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
import store from "../stores";
|
import store from "../stores";
|
||||||
import { setGlobalState } from "../stores/modules/global";
|
import { setGlobalState } from "../stores/modules/global";
|
||||||
import userService from "./userService";
|
|
||||||
|
|
||||||
const globalService = {
|
const globalService = {
|
||||||
getState: () => {
|
getState: () => {
|
||||||
@ -15,12 +14,6 @@ const globalService = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// do nth
|
// do nth
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await userService.initialState();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import globalService from "./globalService";
|
import globalService from "./globalService";
|
||||||
import shortcutService from "./shortcutService";
|
import shortcutService from "./shortcutService";
|
||||||
import userService from "./userService";
|
|
||||||
|
|
||||||
export { globalService, shortcutService, userService };
|
export { globalService, shortcutService };
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
import * as api from "../helpers/api";
|
|
||||||
import store from "../stores";
|
|
||||||
import { setUser, patchUser } from "../stores/modules/user";
|
|
||||||
|
|
||||||
export const convertResponseModelUser = (user: User): User => {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
createdTs: user.createdTs * 1000,
|
|
||||||
updatedTs: user.updatedTs * 1000,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const userService = {
|
|
||||||
getState: () => {
|
|
||||||
return store.getState().user;
|
|
||||||
},
|
|
||||||
|
|
||||||
initialState: async () => {
|
|
||||||
try {
|
|
||||||
const user = (await api.getMyselfUser()).data;
|
|
||||||
if (user) {
|
|
||||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
doSignIn: async () => {
|
|
||||||
const user = (await api.getMyselfUser()).data;
|
|
||||||
if (user) {
|
|
||||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
|
||||||
} else {
|
|
||||||
userService.doSignOut();
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
|
|
||||||
doSignOut: async () => {
|
|
||||||
store.dispatch(setUser());
|
|
||||||
await api.signout();
|
|
||||||
},
|
|
||||||
|
|
||||||
getUserById: async (userId: UserId) => {
|
|
||||||
const user = (await api.getUserById(userId)).data;
|
|
||||||
if (user) {
|
|
||||||
return convertResponseModelUser(user);
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
patchUser: async (userPatch: UserPatch): Promise<void> => {
|
|
||||||
const data = (await api.patchUser(userPatch)).data;
|
|
||||||
if (userPatch.id === store.getState().user.user?.id) {
|
|
||||||
const user = convertResponseModelUser(data);
|
|
||||||
store.dispatch(patchUser(user));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteUser: async (userDelete: UserDelete) => {
|
|
||||||
await api.deleteUser(userDelete);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default userService;
|
|
@ -1,13 +1,11 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import { TypedUseSelectorHook, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useSelector } from "react-redux";
|
||||||
import globalReducer from "./modules/global";
|
import globalReducer from "./modules/global";
|
||||||
import userReducer from "./modules/user";
|
|
||||||
import shortcutReducer from "./modules/shortcut";
|
import shortcutReducer from "./modules/shortcut";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
global: globalReducer,
|
global: globalReducer,
|
||||||
user: userReducer,
|
|
||||||
shortcut: shortcutReducer,
|
shortcut: shortcutReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSlice = createSlice({
|
|
||||||
name: "user",
|
|
||||||
initialState: {} as State,
|
|
||||||
reducers: {
|
|
||||||
setUser: (state, action: PayloadAction<User | undefined>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
user: action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
patchUser: (state, action: PayloadAction<Partial<User>>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
...action.payload,
|
|
||||||
} as User,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { setUser, patchUser } = userSlice.actions;
|
|
||||||
|
|
||||||
export default userSlice.reducer;
|
|
83
web/src/stores/v1/user.ts
Normal file
83
web/src/stores/v1/user.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import * as api from "../../helpers/api";
|
||||||
|
|
||||||
|
const convertResponseModelUser = (user: User): User => {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
createdTs: user.createdTs * 1000,
|
||||||
|
updatedTs: user.updatedTs * 1000,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
userMap: {
|
||||||
|
[key: UserId]: User;
|
||||||
|
};
|
||||||
|
currentUserId?: UserId;
|
||||||
|
fetchUserList: () => Promise<User[]>;
|
||||||
|
fetchCurrentUser: () => Promise<User>;
|
||||||
|
getOrFetchUserById: (id: UserId) => Promise<User>;
|
||||||
|
getUserById: (id: UserId) => User;
|
||||||
|
getCurrentUser: () => User;
|
||||||
|
createUser: (userCreate: UserCreate) => Promise<User>;
|
||||||
|
patchUser: (userPatch: UserPatch) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useUserStore = create<UserState>()((set, get) => ({
|
||||||
|
userMap: {},
|
||||||
|
fetchUserList: async () => {
|
||||||
|
const { data: userList } = await api.getUserList();
|
||||||
|
const userMap = get().userMap;
|
||||||
|
userList.forEach((user) => {
|
||||||
|
userMap[user.id] = convertResponseModelUser(user);
|
||||||
|
});
|
||||||
|
set(userMap);
|
||||||
|
return userList;
|
||||||
|
},
|
||||||
|
fetchCurrentUser: async () => {
|
||||||
|
const { data } = await api.getMyselfUser();
|
||||||
|
const user = convertResponseModelUser(data);
|
||||||
|
const userMap = get().userMap;
|
||||||
|
userMap[user.id] = user;
|
||||||
|
set({ userMap, currentUserId: user.id });
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
getOrFetchUserById: async (id: UserId) => {
|
||||||
|
const userMap = get().userMap;
|
||||||
|
if (userMap[id]) {
|
||||||
|
return userMap[id] as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.getUserById(id);
|
||||||
|
const user = convertResponseModelUser(data);
|
||||||
|
userMap[id] = user;
|
||||||
|
set(userMap);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
createUser: async (userCreate: UserCreate) => {
|
||||||
|
const { data } = await api.createUser(userCreate);
|
||||||
|
const user = convertResponseModelUser(data);
|
||||||
|
const userMap = get().userMap;
|
||||||
|
userMap[user.id] = user;
|
||||||
|
set(userMap);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
patchUser: async (userPatch: UserPatch) => {
|
||||||
|
const { data } = await api.patchUser(userPatch);
|
||||||
|
const user = convertResponseModelUser(data);
|
||||||
|
const userMap = get().userMap;
|
||||||
|
userMap[user.id] = user;
|
||||||
|
set(userMap);
|
||||||
|
},
|
||||||
|
getUserById: (id: UserId) => {
|
||||||
|
const userMap = get().userMap;
|
||||||
|
return userMap[id] as User;
|
||||||
|
},
|
||||||
|
getCurrentUser: () => {
|
||||||
|
const userMap = get().userMap;
|
||||||
|
const currentUserId = get().currentUserId;
|
||||||
|
return userMap[currentUserId as UserId];
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useUserStore;
|
8
web/src/types/modules/user.d.ts
vendored
8
web/src/types/modules/user.d.ts
vendored
@ -14,6 +14,13 @@ interface User {
|
|||||||
role: Role;
|
role: Role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserCreate {
|
||||||
|
email: string;
|
||||||
|
nickname: string;
|
||||||
|
password: string;
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
interface UserPatch {
|
interface UserPatch {
|
||||||
id: UserId;
|
id: UserId;
|
||||||
|
|
||||||
@ -21,6 +28,7 @@ interface UserPatch {
|
|||||||
email?: string;
|
email?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
role?: Role;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDelete {
|
interface UserDelete {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user