Commit c9ab03e1 authored by Steven's avatar Steven

refactor: user service

parent 330282d8
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -12,8 +12,7 @@ var authenticationAllowlistMethods = map[string]bool{ ...@@ -12,8 +12,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.AuthService/SignOut": true, "/memos.api.v1.AuthService/SignOut": true,
"/memos.api.v1.AuthService/SignUp": true, "/memos.api.v1.AuthService/SignUp": true,
"/memos.api.v1.UserService/GetUser": true, "/memos.api.v1.UserService/GetUser": true,
"/memos.api.v1.UserService/GetUserByUsername": true, "/memos.api.v1.UserService/GetUserAvatar": true,
"/memos.api.v1.UserService/GetUserAvatarBinary": true,
"/memos.api.v1.UserService/GetUserStats": true, "/memos.api.v1.UserService/GetUserStats": true,
"/memos.api.v1.UserService/ListAllUserStats": true, "/memos.api.v1.UserService/ListAllUserStats": true,
"/memos.api.v1.UserService/SearchUsers": true, "/memos.api.v1.UserService/SearchUsers": true,
......
...@@ -244,8 +244,7 @@ func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*em ...@@ -244,8 +244,7 @@ func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*em
user, _ := s.GetCurrentUser(ctx) user, _ := s.GetCurrentUser(ctx)
if user != nil { if user != nil {
if _, err := s.DeleteUserAccessToken(ctx, &v1pb.DeleteUserAccessTokenRequest{ if _, err := s.DeleteUserAccessToken(ctx, &v1pb.DeleteUserAccessTokenRequest{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID), Name: fmt.Sprintf("%s%d/accessTokens/%s", UserNamePrefix, user.ID, accessToken),
AccessToken: accessToken,
}); err != nil { }); err != nil {
slog.Error("failed to delete access token", "error", err) slog.Error("failed to delete access token", "error", err)
} }
......
This diff is collapsed.
...@@ -51,51 +51,28 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser ...@@ -51,51 +51,28 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
} }
userStatsMap := map[string]*v1pb.UserStats{}
userMemoStatMap := make(map[int32]*v1pb.UserStats)
for _, memo := range memos { for _, memo := range memos {
creator := fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID)
if _, ok := userStatsMap[creator]; !ok {
userStatsMap[creator] = &v1pb.UserStats{
Name: creator,
MemoDisplayTimestamps: []*timestamppb.Timestamp{},
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{},
TagCount: map[string]int32{},
}
}
displayTs := memo.CreatedTs displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs displayTs = memo.UpdatedTs
} }
userStats := userStatsMap[creator] userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{
userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) Name: fmt.Sprintf("users/%d/stats", memo.CreatorID),
// Handle duplicated tags.
for _, tag := range memo.Payload.Tags {
userStats.TagCount[tag]++
}
if memo.Pinned {
userStats.PinnedMemos = append(userStats.PinnedMemos, fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID))
}
if memo.Payload.Property.GetHasLink() {
userStats.MemoTypeStats.LinkCount++
}
if memo.Payload.Property.GetHasCode() {
userStats.MemoTypeStats.CodeCount++
}
if memo.Payload.Property.GetHasTaskList() {
userStats.MemoTypeStats.TodoCount++
} }
if memo.Payload.Property.GetHasIncompleteTasks() { userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps = append(userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
userStats.MemoTypeStats.UndoCount++
} }
userStats.TotalMemoCount++
userMemoStats := []*v1pb.UserStats{}
for _, userMemoStat := range userMemoStatMap {
userMemoStats = append(userMemoStats, userMemoStat)
} }
userStatsList := []*v1pb.UserStats{}
for _, userStats := range userStatsMap { response := &v1pb.ListAllUserStatsResponse{
userStatsList = append(userStatsList, userStats) UserStats: userMemoStats,
} }
return &v1pb.ListAllUserStatsResponse{ return response, nil
UserStats: userStatsList,
}, nil
} }
func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) { func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) {
...@@ -103,32 +80,27 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt ...@@ -103,32 +80,27 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
} }
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
currentUser, err := s.GetCurrentUser(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
} }
normalStatus := store.Normal normalStatus := store.Normal
memoFind := &store.FindMemo{ memoFind := &store.FindMemo{
CreatorID: &userID,
// Exclude comments by default. // Exclude comments by default.
ExcludeComments: true, ExcludeComments: true,
ExcludeContent: true, ExcludeContent: true,
CreatorID: &userID,
RowStatus: &normalStatus, RowStatus: &normalStatus,
} }
currentUser, err := s.GetCurrentUser(ctx) if currentUser == nil {
if err != nil { memoFind.VisibilityList = []store.Visibility{store.Public}
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) } else if currentUser.ID != userID {
} memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
visibilities := []store.Visibility{store.Public}
if currentUser != nil {
visibilities = append(visibilities, store.Protected)
if currentUser.ID == user.ID {
visibilities = append(visibilities, store.Private)
}
} }
memoFind.VisibilityList = visibilities
memos, err := s.Store.ListMemos(ctx, memoFind) memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
...@@ -138,38 +110,56 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt ...@@ -138,38 +110,56 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting") return nil, errors.Wrap(err, "failed to get workspace memo related setting")
} }
userStats := &v1pb.UserStats{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID), displayTimestamps := []*timestamppb.Timestamp{}
MemoDisplayTimestamps: []*timestamppb.Timestamp{}, tagCount := make(map[string]int32)
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{}, linkCount := int32(0)
TagCount: map[string]int32{}, codeCount := int32(0)
TotalMemoCount: int32(len(memos)), todoCount := int32(0)
} undoCount := int32(0)
pinnedMemos := []string{}
for _, memo := range memos { for _, memo := range memos {
displayTs := memo.CreatedTs displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs displayTs = memo.UpdatedTs
} }
userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) displayTimestamps = append(displayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
// Handle duplicated tags.
for _, tag := range memo.Payload.Tags { // Count different memo types based on content
userStats.TagCount[tag]++ if memo.Payload != nil && memo.Payload.Property != nil {
if memo.Payload.Property.HasLink {
linkCount++
} }
if memo.Pinned { if memo.Payload.Property.HasCode {
userStats.PinnedMemos = append(userStats.PinnedMemos, fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)) codeCount++
} }
if memo.Payload.Property.GetHasLink() { if memo.Payload.Property.HasTaskList {
userStats.MemoTypeStats.LinkCount++ todoCount++
} }
if memo.Payload.Property.GetHasCode() { if memo.Payload.Property.HasIncompleteTasks {
userStats.MemoTypeStats.CodeCount++ undoCount++
} }
if memo.Payload.Property.GetHasTaskList() {
userStats.MemoTypeStats.TodoCount++
} }
if memo.Payload.Property.GetHasIncompleteTasks() {
userStats.MemoTypeStats.UndoCount++ if memo.Pinned {
pinnedMemos = append(pinnedMemos, fmt.Sprintf("users/%d/memos/%d", userID, memo.ID))
} }
} }
userStats := &v1pb.UserStats{
Name: fmt.Sprintf("users/%d/stats", userID),
MemoDisplayTimestamps: displayTimestamps,
TagCount: tagCount,
PinnedMemos: pinnedMemos,
TotalMemoCount: int32(len(memos)),
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{
LinkCount: linkCount,
CodeCount: codeCount,
TodoCount: todoCount,
UndoCount: undoCount,
},
}
return userStats, nil return userStats, nil
} }
...@@ -67,7 +67,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => { ...@@ -67,7 +67,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
<div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"> <div className="max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg">
<div className="flex flex-row justify-between items-center mb-4 gap-2 w-full"> <div className="flex flex-row justify-between items-center mb-4 gap-2 w-full">
<p> <p>
{t("setting.account-section.change-password")} ({user.nickname}) {t("setting.account-section.change-password")} ({user.displayName})
</p> </p>
<Button variant="plain" onClick={handleCloseBtnClick}> <Button variant="plain" onClick={handleCloseBtnClick}>
<XIcon className="w-5 h-auto" /> <XIcon className="w-5 h-auto" />
......
...@@ -70,9 +70,11 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => { ...@@ -70,9 +70,11 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
try { try {
await userServiceClient.createUserAccessToken({ await userServiceClient.createUserAccessToken({
name: currentUser.name, parent: currentUser.name,
accessToken: {
description: state.description, description: state.description,
expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined, expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined,
},
}); });
onConfirm(); onConfirm();
......
...@@ -109,7 +109,7 @@ const MemoCommentMessage = observer(({ inbox }: Props) => { ...@@ -109,7 +109,7 @@ const MemoCommentMessage = observer(({ inbox }: Props) => {
onClick={handleNavigateToMemo} onClick={handleNavigateToMemo}
> >
{t("inbox.memo-comment", { {t("inbox.memo-comment", {
user: sender?.nickname || sender?.username, user: sender?.displayName || sender?.username,
memo: relatedMemo?.name, memo: relatedMemo?.name,
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
})} })}
......
...@@ -148,7 +148,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => { ...@@ -148,7 +148,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
to={`/u/${encodeURIComponent(creator.username)}`} to={`/u/${encodeURIComponent(creator.username)}`}
viewTransition viewTransition
> >
{creator.nickname || creator.username} {creator.displayName || creator.username}
</Link> </Link>
<div <div
className="w-auto -mt-0.5 text-xs leading-tight text-gray-400 dark:text-gray-500 select-none cursor-pointer" className="w-auto -mt-0.5 text-xs leading-tight text-gray-400 dark:text-gray-500 select-none cursor-pointer"
......
...@@ -19,12 +19,12 @@ const stringifyUsers = (users: User[], reactionType: string): string => { ...@@ -19,12 +19,12 @@ const stringifyUsers = (users: User[], reactionType: string): string => {
return ""; return "";
} }
if (users.length < 5) { if (users.length < 5) {
return users.map((user) => user.nickname || user.username).join(", ") + " reacted with " + reactionType.toLowerCase(); return users.map((user) => user.displayName || user.username).join(", ") + " reacted with " + reactionType.toLowerCase();
} }
return ( return (
`${users `${users
.slice(0, 4) .slice(0, 4)
.map((user) => user.nickname || user.username) .map((user) => user.displayName || user.username)
.join(", ")} and ${users.length - 4} more reacted with ` + reactionType.toLowerCase() .join(", ")} and ${users.length - 4} more reacted with ` + reactionType.toLowerCase()
); );
}; };
......
...@@ -10,8 +10,8 @@ import { useTranslate } from "@/utils/i18n"; ...@@ -10,8 +10,8 @@ import { useTranslate } from "@/utils/i18n";
import showCreateAccessTokenDialog from "../CreateAccessTokenDialog"; import showCreateAccessTokenDialog from "../CreateAccessTokenDialog";
import LearnMore from "../LearnMore"; import LearnMore from "../LearnMore";
const listAccessTokens = async (name: string) => { const listAccessTokens = async (parent: string) => {
const { accessTokens } = await userServiceClient.listUserAccessTokens({ name }); const { accessTokens } = await userServiceClient.listUserAccessTokens({ parent });
return accessTokens.sort((a, b) => (b.issuedAt?.getTime() ?? 0) - (a.issuedAt?.getTime() ?? 0)); return accessTokens.sort((a, b) => (b.issuedAt?.getTime() ?? 0) - (a.issuedAt?.getTime() ?? 0));
}; };
...@@ -36,12 +36,12 @@ const AccessTokenSection = () => { ...@@ -36,12 +36,12 @@ const AccessTokenSection = () => {
toast.success(t("setting.access-token-section.access-token-copied-to-clipboard")); toast.success(t("setting.access-token-section.access-token-copied-to-clipboard"));
}; };
const handleDeleteAccessToken = async (accessToken: string) => { const handleDeleteAccessToken = async (userAccessToken: UserAccessToken) => {
const formatedAccessToken = getFormatedAccessToken(accessToken); const formatedAccessToken = getFormatedAccessToken(userAccessToken.accessToken);
const confirmed = window.confirm(t("setting.access-token-section.access-token-deletion", { accessToken: formatedAccessToken })); const confirmed = window.confirm(t("setting.access-token-section.access-token-deletion", { accessToken: formatedAccessToken }));
if (confirmed) { if (confirmed) {
await userServiceClient.deleteUserAccessToken({ name: currentUser.name, accessToken: accessToken }); await userServiceClient.deleteUserAccessToken({ name: userAccessToken.name });
setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== accessToken)); setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== userAccessToken.accessToken));
} }
}; };
...@@ -116,7 +116,7 @@ const AccessTokenSection = () => { ...@@ -116,7 +116,7 @@ const AccessTokenSection = () => {
<Button <Button
variant="plain" variant="plain"
onClick={() => { onClick={() => {
handleDeleteAccessToken(userAccessToken.accessToken); handleDeleteAccessToken(userAccessToken);
}} }}
> >
<TrashIcon className="text-red-600 w-4 h-auto" /> <TrashIcon className="text-red-600 w-4 h-auto" />
......
...@@ -109,7 +109,7 @@ const MemberSection = observer(() => { ...@@ -109,7 +109,7 @@ const MemberSection = observer(() => {
}; };
const handleArchiveUserClick = async (user: User) => { const handleArchiveUserClick = async (user: User) => {
const confirmed = window.confirm(t("setting.member-section.archive-warning", { username: user.nickname })); const confirmed = window.confirm(t("setting.member-section.archive-warning", { username: user.displayName }));
if (confirmed) { if (confirmed) {
await userServiceClient.updateUser({ await userServiceClient.updateUser({
user: { user: {
...@@ -134,7 +134,7 @@ const MemberSection = observer(() => { ...@@ -134,7 +134,7 @@ const MemberSection = observer(() => {
}; };
const handleDeleteUserClick = async (user: User) => { const handleDeleteUserClick = async (user: User) => {
const confirmed = window.confirm(t("setting.member-section.delete-warning", { username: user.nickname })); const confirmed = window.confirm(t("setting.member-section.delete-warning", { username: user.displayName }));
if (confirmed) { if (confirmed) {
await userStore.deleteUser(user.name); await userStore.deleteUser(user.name);
fetchUsers(); fetchUsers();
...@@ -209,7 +209,7 @@ const MemberSection = observer(() => { ...@@ -209,7 +209,7 @@ const MemberSection = observer(() => {
<span className="ml-1 italic">{user.state === State.ARCHIVED && "(Archived)"}</span> <span className="ml-1 italic">{user.state === State.ARCHIVED && "(Archived)"}</span>
</td> </td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{stringifyUserRole(user.role)}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{stringifyUserRole(user.role)}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{user.nickname}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{user.displayName}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{user.email}</td> <td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500 dark:text-gray-400">{user.email}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end"> <td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
{currentUser?.name === user.name ? ( {currentUser?.name === user.name ? (
......
...@@ -19,7 +19,7 @@ const MyAccountSection = () => { ...@@ -19,7 +19,7 @@ const MyAccountSection = () => {
<UserAvatar className="mr-2 shrink-0 w-10 h-10" avatarUrl={user.avatarUrl} /> <UserAvatar className="mr-2 shrink-0 w-10 h-10" avatarUrl={user.avatarUrl} />
<div className="max-w-[calc(100%-3rem)] flex flex-col justify-center items-start"> <div className="max-w-[calc(100%-3rem)] flex flex-col justify-center items-start">
<p className="w-full"> <p className="w-full">
<span className="text-xl leading-tight font-medium">{user.nickname}</span> <span className="text-xl leading-tight font-medium">{user.displayName}</span>
<span className="ml-1 text-base leading-tight text-gray-500 dark:text-gray-400">({user.username})</span> <span className="ml-1 text-base leading-tight text-gray-500 dark:text-gray-400">({user.username})</span>
</p> </p>
<p className="w-4/5 leading-tight text-sm truncate">{user.description}</p> <p className="w-4/5 leading-tight text-sm truncate">{user.description}</p>
......
This diff is collapsed.
...@@ -40,7 +40,7 @@ const UserBanner = (props: Props) => { ...@@ -40,7 +40,7 @@ const UserBanner = (props: Props) => {
)} )}
{!collapsed && ( {!collapsed && (
<span className="ml-2 text-lg font-medium text-slate-800 dark:text-gray-300 grow truncate"> <span className="ml-2 text-lg font-medium text-slate-800 dark:text-gray-300 grow truncate">
{currentUser.nickname || currentUser.username} {currentUser.displayName || currentUser.username}
</span> </span>
)} )}
</div> </div>
......
...@@ -90,7 +90,7 @@ const UserProfile = observer(() => { ...@@ -90,7 +90,7 @@ const UserProfile = observer(() => {
<UserAvatar className="w-16! h-16! drop-shadow rounded-3xl" avatarUrl={user?.avatarUrl} /> <UserAvatar className="w-16! h-16! drop-shadow rounded-3xl" avatarUrl={user?.avatarUrl} />
<div className="mt-2 w-auto max-w-[calc(100%-6rem)] flex flex-col justify-center items-start"> <div className="mt-2 w-auto max-w-[calc(100%-6rem)] flex flex-col justify-center items-start">
<p className="w-full text-3xl text-black leading-tight font-medium opacity-80 dark:text-gray-200 truncate"> <p className="w-full text-3xl text-black leading-tight font-medium opacity-80 dark:text-gray-200 truncate">
{user.nickname || user.username} {user.displayName || user.username}
</p> </p>
<p className="w-full text-gray-500 leading-snug dark:text-gray-400 whitespace-pre-wrap truncate line-clamp-6"> <p className="w-full text-gray-500 leading-snug dark:text-gray-400 whitespace-pre-wrap truncate line-clamp-6">
{user.description} {user.description}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment