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{
"/memos.api.v1.AuthService/SignOut": true,
"/memos.api.v1.AuthService/SignUp": true,
"/memos.api.v1.UserService/GetUser": true,
"/memos.api.v1.UserService/GetUserByUsername": true,
"/memos.api.v1.UserService/GetUserAvatarBinary": true,
"/memos.api.v1.UserService/GetUserAvatar": true,
"/memos.api.v1.UserService/GetUserStats": true,
"/memos.api.v1.UserService/ListAllUserStats": true,
"/memos.api.v1.UserService/SearchUsers": true,
......
......@@ -244,8 +244,7 @@ func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*em
user, _ := s.GetCurrentUser(ctx)
if user != nil {
if _, err := s.DeleteUserAccessToken(ctx, &v1pb.DeleteUserAccessTokenRequest{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
AccessToken: accessToken,
Name: fmt.Sprintf("%s%d/accessTokens/%s", UserNamePrefix, user.ID, accessToken),
}); err != nil {
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
if err != nil {
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 {
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
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
userStats := userStatsMap[creator]
userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
// 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++
userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{
Name: fmt.Sprintf("users/%d/stats", memo.CreatorID),
}
if memo.Payload.Property.GetHasCode() {
userStats.MemoTypeStats.CodeCount++
}
if memo.Payload.Property.GetHasTaskList() {
userStats.MemoTypeStats.TodoCount++
}
if memo.Payload.Property.GetHasIncompleteTasks() {
userStats.MemoTypeStats.UndoCount++
}
userStats.TotalMemoCount++
userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps = append(userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
}
userStatsList := []*v1pb.UserStats{}
for _, userStats := range userStatsMap {
userStatsList = append(userStatsList, userStats)
userMemoStats := []*v1pb.UserStats{}
for _, userMemoStat := range userMemoStatMap {
userMemoStats = append(userMemoStats, userMemoStat)
}
return &v1pb.ListAllUserStatsResponse{
UserStats: userStatsList,
}, nil
response := &v1pb.ListAllUserStatsResponse{
UserStats: userMemoStats,
}
return response, nil
}
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
if err != nil {
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 {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
normalStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &userID,
// Exclude comments by default.
ExcludeComments: true,
ExcludeContent: true,
CreatorID: &userID,
RowStatus: &normalStatus,
}
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
visibilities := []store.Visibility{store.Public}
if currentUser != nil {
visibilities = append(visibilities, store.Protected)
if currentUser.ID == user.ID {
visibilities = append(visibilities, store.Private)
}
if currentUser == nil {
memoFind.VisibilityList = []store.Visibility{store.Public}
} else if currentUser.ID != userID {
memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
memoFind.VisibilityList = visibilities
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
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
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
userStats := &v1pb.UserStats{
Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
MemoDisplayTimestamps: []*timestamppb.Timestamp{},
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{},
TagCount: map[string]int32{},
TotalMemoCount: int32(len(memos)),
}
displayTimestamps := []*timestamppb.Timestamp{}
tagCount := make(map[string]int32)
linkCount := int32(0)
codeCount := int32(0)
todoCount := int32(0)
undoCount := int32(0)
pinnedMemos := []string{}
for _, memo := range memos {
displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
userStats.MemoDisplayTimestamps = append(userStats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
// Handle duplicated tags.
for _, tag := range memo.Payload.Tags {
userStats.TagCount[tag]++
displayTimestamps = append(displayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
// Count different memo types based on content
if memo.Payload != nil && memo.Payload.Property != nil {
if memo.Payload.Property.HasLink {
linkCount++
}
if memo.Payload.Property.HasCode {
codeCount++
}
if memo.Payload.Property.HasTaskList {
todoCount++
}
if memo.Payload.Property.HasIncompleteTasks {
undoCount++
}
}
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() {
userStats.MemoTypeStats.UndoCount++
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
}
......@@ -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="flex flex-row justify-between items-center mb-4 gap-2 w-full">
<p>
{t("setting.account-section.change-password")} ({user.nickname})
{t("setting.account-section.change-password")} ({user.displayName})
</p>
<Button variant="plain" onClick={handleCloseBtnClick}>
<XIcon className="w-5 h-auto" />
......
......@@ -70,9 +70,11 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
try {
await userServiceClient.createUserAccessToken({
name: currentUser.name,
description: state.description,
expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined,
parent: currentUser.name,
accessToken: {
description: state.description,
expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined,
},
});
onConfirm();
......
......@@ -109,7 +109,7 @@ const MemoCommentMessage = observer(({ inbox }: Props) => {
onClick={handleNavigateToMemo}
>
{t("inbox.memo-comment", {
user: sender?.nickname || sender?.username,
user: sender?.displayName || sender?.username,
memo: relatedMemo?.name,
interpolation: { escapeValue: false },
})}
......
......@@ -148,7 +148,7 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
to={`/u/${encodeURIComponent(creator.username)}`}
viewTransition
>
{creator.nickname || creator.username}
{creator.displayName || creator.username}
</Link>
<div
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 => {
return "";
}
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 (
`${users
.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()
);
};
......
......@@ -10,8 +10,8 @@ import { useTranslate } from "@/utils/i18n";
import showCreateAccessTokenDialog from "../CreateAccessTokenDialog";
import LearnMore from "../LearnMore";
const listAccessTokens = async (name: string) => {
const { accessTokens } = await userServiceClient.listUserAccessTokens({ name });
const listAccessTokens = async (parent: string) => {
const { accessTokens } = await userServiceClient.listUserAccessTokens({ parent });
return accessTokens.sort((a, b) => (b.issuedAt?.getTime() ?? 0) - (a.issuedAt?.getTime() ?? 0));
};
......@@ -36,12 +36,12 @@ const AccessTokenSection = () => {
toast.success(t("setting.access-token-section.access-token-copied-to-clipboard"));
};
const handleDeleteAccessToken = async (accessToken: string) => {
const formatedAccessToken = getFormatedAccessToken(accessToken);
const handleDeleteAccessToken = async (userAccessToken: UserAccessToken) => {
const formatedAccessToken = getFormatedAccessToken(userAccessToken.accessToken);
const confirmed = window.confirm(t("setting.access-token-section.access-token-deletion", { accessToken: formatedAccessToken }));
if (confirmed) {
await userServiceClient.deleteUserAccessToken({ name: currentUser.name, accessToken: accessToken });
setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== accessToken));
await userServiceClient.deleteUserAccessToken({ name: userAccessToken.name });
setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== userAccessToken.accessToken));
}
};
......@@ -116,7 +116,7 @@ const AccessTokenSection = () => {
<Button
variant="plain"
onClick={() => {
handleDeleteAccessToken(userAccessToken.accessToken);
handleDeleteAccessToken(userAccessToken);
}}
>
<TrashIcon className="text-red-600 w-4 h-auto" />
......
......@@ -109,7 +109,7 @@ const MemberSection = observer(() => {
};
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) {
await userServiceClient.updateUser({
user: {
......@@ -134,7 +134,7 @@ const MemberSection = observer(() => {
};
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) {
await userStore.deleteUser(user.name);
fetchUsers();
......@@ -209,7 +209,7 @@ const MemberSection = observer(() => {
<span className="ml-1 italic">{user.state === State.ARCHIVED && "(Archived)"}</span>
</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="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
{currentUser?.name === user.name ? (
......
......@@ -19,7 +19,7 @@ const MyAccountSection = () => {
<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">
<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>
</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) => {
)}
{!collapsed && (
<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>
)}
</div>
......
......@@ -90,7 +90,7 @@ const UserProfile = observer(() => {
<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">
<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 className="w-full text-gray-500 leading-snug dark:text-gray-400 whitespace-pre-wrap truncate line-clamp-6">
{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