user_service.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. package v1
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "net/http"
  7. "regexp"
  8. "slices"
  9. "strings"
  10. "time"
  11. "github.com/golang-jwt/jwt/v5"
  12. "github.com/labstack/echo/v4"
  13. "github.com/pkg/errors"
  14. "golang.org/x/crypto/bcrypt"
  15. "google.golang.org/genproto/googleapis/api/httpbody"
  16. "google.golang.org/grpc/codes"
  17. "google.golang.org/grpc/status"
  18. "google.golang.org/protobuf/types/known/emptypb"
  19. "google.golang.org/protobuf/types/known/timestamppb"
  20. "github.com/usememos/memos/internal/util"
  21. v1pb "github.com/usememos/memos/proto/gen/api/v1"
  22. storepb "github.com/usememos/memos/proto/gen/store"
  23. "github.com/usememos/memos/store"
  24. )
  25. func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
  26. currentUser, err := s.GetCurrentUser(ctx)
  27. if err != nil {
  28. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  29. }
  30. if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
  31. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  32. }
  33. users, err := s.Store.ListUsers(ctx, &store.FindUser{})
  34. if err != nil {
  35. return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
  36. }
  37. response := &v1pb.ListUsersResponse{
  38. Users: []*v1pb.User{},
  39. }
  40. for _, user := range users {
  41. response.Users = append(response.Users, convertUserFromStore(user))
  42. }
  43. return response, nil
  44. }
  45. func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest) (*v1pb.User, error) {
  46. userID, err := ExtractUserIDFromName(request.Name)
  47. if err != nil {
  48. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  49. }
  50. user, err := s.Store.GetUser(ctx, &store.FindUser{
  51. ID: &userID,
  52. })
  53. if err != nil {
  54. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  55. }
  56. if user == nil {
  57. return nil, status.Errorf(codes.NotFound, "user not found")
  58. }
  59. return convertUserFromStore(user), nil
  60. }
  61. func (s *APIV1Service) GetUserByUsername(ctx context.Context, request *v1pb.GetUserByUsernameRequest) (*v1pb.User, error) {
  62. user, err := s.Store.GetUser(ctx, &store.FindUser{
  63. Username: &request.Username,
  64. })
  65. if err != nil {
  66. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  67. }
  68. if user == nil {
  69. return nil, status.Errorf(codes.NotFound, "user not found")
  70. }
  71. return convertUserFromStore(user), nil
  72. }
  73. func (s *APIV1Service) GetUserAvatarBinary(ctx context.Context, request *v1pb.GetUserAvatarBinaryRequest) (*httpbody.HttpBody, error) {
  74. userID, err := ExtractUserIDFromName(request.Name)
  75. if err != nil {
  76. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  77. }
  78. user, err := s.Store.GetUser(ctx, &store.FindUser{
  79. ID: &userID,
  80. })
  81. if err != nil {
  82. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  83. }
  84. if user == nil {
  85. return nil, status.Errorf(codes.NotFound, "user not found")
  86. }
  87. if user.AvatarURL == "" {
  88. return nil, status.Errorf(codes.NotFound, "avatar not found")
  89. }
  90. imageType, base64Data, err := extractImageInfo(user.AvatarURL)
  91. if err != nil {
  92. return nil, status.Errorf(codes.Internal, "failed to extract image info: %v", err)
  93. }
  94. imageData, err := base64.StdEncoding.DecodeString(base64Data)
  95. if err != nil {
  96. return nil, status.Errorf(codes.Internal, "failed to decode string: %v", err)
  97. }
  98. httpBody := &httpbody.HttpBody{
  99. ContentType: imageType,
  100. Data: imageData,
  101. }
  102. return httpBody, nil
  103. }
  104. func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
  105. currentUser, err := s.GetCurrentUser(ctx)
  106. if err != nil {
  107. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  108. }
  109. if currentUser.Role != store.RoleHost {
  110. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  111. }
  112. if !util.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {
  113. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
  114. }
  115. passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
  116. if err != nil {
  117. return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
  118. }
  119. user, err := s.Store.CreateUser(ctx, &store.User{
  120. Username: request.User.Username,
  121. Role: convertUserRoleToStore(request.User.Role),
  122. Email: request.User.Email,
  123. Nickname: request.User.Nickname,
  124. PasswordHash: string(passwordHash),
  125. })
  126. if err != nil {
  127. return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
  128. }
  129. return convertUserFromStore(user), nil
  130. }
  131. func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserRequest) (*v1pb.User, error) {
  132. workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
  133. if err != nil {
  134. return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err)
  135. }
  136. userID, err := ExtractUserIDFromName(request.User.Name)
  137. if err != nil {
  138. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  139. }
  140. currentUser, err := s.GetCurrentUser(ctx)
  141. if err != nil {
  142. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  143. }
  144. if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
  145. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  146. }
  147. if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
  148. return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
  149. }
  150. user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
  151. if err != nil {
  152. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  153. }
  154. if user == nil {
  155. return nil, status.Errorf(codes.NotFound, "user not found")
  156. }
  157. currentTs := time.Now().Unix()
  158. update := &store.UpdateUser{
  159. ID: user.ID,
  160. UpdatedTs: &currentTs,
  161. }
  162. for _, field := range request.UpdateMask.Paths {
  163. if field == "username" {
  164. if workspaceGeneralSetting.DisallowChangeUsername {
  165. return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change username")
  166. }
  167. if !util.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {
  168. return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
  169. }
  170. update.Username = &request.User.Username
  171. } else if field == "nickname" {
  172. if workspaceGeneralSetting.DisallowChangeNickname {
  173. return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change nickname")
  174. }
  175. update.Nickname = &request.User.Nickname
  176. } else if field == "email" {
  177. update.Email = &request.User.Email
  178. } else if field == "avatar_url" {
  179. update.AvatarURL = &request.User.AvatarUrl
  180. } else if field == "description" {
  181. update.Description = &request.User.Description
  182. } else if field == "role" {
  183. role := convertUserRoleToStore(request.User.Role)
  184. update.Role = &role
  185. } else if field == "password" {
  186. passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
  187. if err != nil {
  188. return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
  189. }
  190. passwordHashStr := string(passwordHash)
  191. update.PasswordHash = &passwordHashStr
  192. } else if field == "state" {
  193. rowStatus := convertStateToStore(request.User.State)
  194. update.RowStatus = &rowStatus
  195. } else {
  196. return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
  197. }
  198. }
  199. updatedUser, err := s.Store.UpdateUser(ctx, update)
  200. if err != nil {
  201. return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
  202. }
  203. return convertUserFromStore(updatedUser), nil
  204. }
  205. func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserRequest) (*emptypb.Empty, error) {
  206. userID, err := ExtractUserIDFromName(request.Name)
  207. if err != nil {
  208. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  209. }
  210. currentUser, err := s.GetCurrentUser(ctx)
  211. if err != nil {
  212. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  213. }
  214. if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
  215. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  216. }
  217. user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
  218. if err != nil {
  219. return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
  220. }
  221. if user == nil {
  222. return nil, status.Errorf(codes.NotFound, "user not found")
  223. }
  224. if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
  225. ID: user.ID,
  226. }); err != nil {
  227. return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err)
  228. }
  229. return &emptypb.Empty{}, nil
  230. }
  231. func getDefaultUserSetting(workspaceMemoRelatedSetting *storepb.WorkspaceMemoRelatedSetting) *v1pb.UserSetting {
  232. defaultVisibility := "PRIVATE"
  233. if workspaceMemoRelatedSetting.DefaultVisibility != "" {
  234. defaultVisibility = workspaceMemoRelatedSetting.DefaultVisibility
  235. }
  236. return &v1pb.UserSetting{
  237. Locale: "en",
  238. Appearance: "system",
  239. MemoVisibility: defaultVisibility,
  240. }
  241. }
  242. func (s *APIV1Service) GetUserSetting(ctx context.Context, _ *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) {
  243. user, err := s.GetCurrentUser(ctx)
  244. if err != nil {
  245. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  246. }
  247. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
  248. if err != nil {
  249. return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err)
  250. }
  251. userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{
  252. UserID: &user.ID,
  253. })
  254. if err != nil {
  255. return nil, status.Errorf(codes.Internal, "failed to list user settings: %v", err)
  256. }
  257. // getDefaultUserSetting By workspaceSetting
  258. userSettingMessage := getDefaultUserSetting(workspaceMemoRelatedSetting)
  259. for _, setting := range userSettings {
  260. if setting.Key == storepb.UserSettingKey_LOCALE {
  261. userSettingMessage.Locale = setting.GetLocale()
  262. } else if setting.Key == storepb.UserSettingKey_APPEARANCE {
  263. userSettingMessage.Appearance = setting.GetAppearance()
  264. } else if setting.Key == storepb.UserSettingKey_MEMO_VISIBILITY {
  265. userSettingMessage.MemoVisibility = setting.GetMemoVisibility()
  266. }
  267. }
  268. return userSettingMessage, nil
  269. }
  270. func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) {
  271. user, err := s.GetCurrentUser(ctx)
  272. if err != nil {
  273. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  274. }
  275. if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
  276. return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
  277. }
  278. for _, field := range request.UpdateMask.Paths {
  279. if field == "locale" {
  280. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  281. UserId: user.ID,
  282. Key: storepb.UserSettingKey_LOCALE,
  283. Value: &storepb.UserSetting_Locale{
  284. Locale: request.Setting.Locale,
  285. },
  286. }); err != nil {
  287. return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
  288. }
  289. } else if field == "appearance" {
  290. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  291. UserId: user.ID,
  292. Key: storepb.UserSettingKey_APPEARANCE,
  293. Value: &storepb.UserSetting_Appearance{
  294. Appearance: request.Setting.Appearance,
  295. },
  296. }); err != nil {
  297. return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
  298. }
  299. } else if field == "memo_visibility" {
  300. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  301. UserId: user.ID,
  302. Key: storepb.UserSettingKey_MEMO_VISIBILITY,
  303. Value: &storepb.UserSetting_MemoVisibility{
  304. MemoVisibility: request.Setting.MemoVisibility,
  305. },
  306. }); err != nil {
  307. return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
  308. }
  309. } else {
  310. return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
  311. }
  312. }
  313. return s.GetUserSetting(ctx, &v1pb.GetUserSettingRequest{})
  314. }
  315. func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.ListUserAccessTokensRequest) (*v1pb.ListUserAccessTokensResponse, error) {
  316. userID, err := ExtractUserIDFromName(request.Name)
  317. if err != nil {
  318. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  319. }
  320. currentUser, err := s.GetCurrentUser(ctx)
  321. if err != nil {
  322. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  323. }
  324. if currentUser == nil {
  325. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  326. }
  327. if currentUser.ID != userID {
  328. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  329. }
  330. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
  331. if err != nil {
  332. return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
  333. }
  334. accessTokens := []*v1pb.UserAccessToken{}
  335. for _, userAccessToken := range userAccessTokens {
  336. claims := &ClaimsMessage{}
  337. _, err := jwt.ParseWithClaims(userAccessToken.AccessToken, claims, func(t *jwt.Token) (any, error) {
  338. if t.Method.Alg() != jwt.SigningMethodHS256.Name {
  339. return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
  340. }
  341. if kid, ok := t.Header["kid"].(string); ok {
  342. if kid == "v1" {
  343. return []byte(s.Secret), nil
  344. }
  345. }
  346. return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
  347. })
  348. if err != nil {
  349. // If the access token is invalid or expired, just ignore it.
  350. continue
  351. }
  352. userAccessToken := &v1pb.UserAccessToken{
  353. AccessToken: userAccessToken.AccessToken,
  354. Description: userAccessToken.Description,
  355. IssuedAt: timestamppb.New(claims.IssuedAt.Time),
  356. }
  357. if claims.ExpiresAt != nil {
  358. userAccessToken.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time)
  359. }
  360. accessTokens = append(accessTokens, userAccessToken)
  361. }
  362. // Sort by issued time in descending order.
  363. slices.SortFunc(accessTokens, func(i, j *v1pb.UserAccessToken) int {
  364. return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds)
  365. })
  366. response := &v1pb.ListUserAccessTokensResponse{
  367. AccessTokens: accessTokens,
  368. }
  369. return response, nil
  370. }
  371. func (s *APIV1Service) CreateUserAccessToken(ctx context.Context, request *v1pb.CreateUserAccessTokenRequest) (*v1pb.UserAccessToken, error) {
  372. userID, err := ExtractUserIDFromName(request.Name)
  373. if err != nil {
  374. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  375. }
  376. currentUser, err := s.GetCurrentUser(ctx)
  377. if err != nil {
  378. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  379. }
  380. if currentUser == nil {
  381. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  382. }
  383. if currentUser.ID != userID {
  384. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  385. }
  386. expiresAt := time.Time{}
  387. if request.ExpiresAt != nil {
  388. expiresAt = request.ExpiresAt.AsTime()
  389. }
  390. accessToken, err := GenerateAccessToken(currentUser.Username, currentUser.ID, expiresAt, []byte(s.Secret))
  391. if err != nil {
  392. return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err)
  393. }
  394. claims := &ClaimsMessage{}
  395. _, err = jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
  396. if t.Method.Alg() != jwt.SigningMethodHS256.Name {
  397. return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
  398. }
  399. if kid, ok := t.Header["kid"].(string); ok {
  400. if kid == "v1" {
  401. return []byte(s.Secret), nil
  402. }
  403. }
  404. return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
  405. })
  406. if err != nil {
  407. return nil, status.Errorf(codes.Internal, "failed to parse access token: %v", err)
  408. }
  409. // Upsert the access token to user setting store.
  410. if err := s.UpsertAccessTokenToStore(ctx, currentUser, accessToken, request.Description); err != nil {
  411. return nil, status.Errorf(codes.Internal, "failed to upsert access token to store: %v", err)
  412. }
  413. userAccessToken := &v1pb.UserAccessToken{
  414. AccessToken: accessToken,
  415. Description: request.Description,
  416. IssuedAt: timestamppb.New(claims.IssuedAt.Time),
  417. }
  418. if claims.ExpiresAt != nil {
  419. userAccessToken.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time)
  420. }
  421. return userAccessToken, nil
  422. }
  423. func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.DeleteUserAccessTokenRequest) (*emptypb.Empty, error) {
  424. userID, err := ExtractUserIDFromName(request.Name)
  425. if err != nil {
  426. return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
  427. }
  428. currentUser, err := s.GetCurrentUser(ctx)
  429. if err != nil {
  430. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  431. }
  432. if currentUser == nil {
  433. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  434. }
  435. if currentUser.ID != userID {
  436. return nil, status.Errorf(codes.PermissionDenied, "permission denied")
  437. }
  438. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, currentUser.ID)
  439. if err != nil {
  440. return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
  441. }
  442. updatedUserAccessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
  443. for _, userAccessToken := range userAccessTokens {
  444. if userAccessToken.AccessToken == request.AccessToken {
  445. continue
  446. }
  447. updatedUserAccessTokens = append(updatedUserAccessTokens, userAccessToken)
  448. }
  449. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  450. UserId: currentUser.ID,
  451. Key: storepb.UserSettingKey_ACCESS_TOKENS,
  452. Value: &storepb.UserSetting_AccessTokens{
  453. AccessTokens: &storepb.AccessTokensUserSetting{
  454. AccessTokens: updatedUserAccessTokens,
  455. },
  456. },
  457. }); err != nil {
  458. return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
  459. }
  460. return &emptypb.Empty{}, nil
  461. }
  462. func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error {
  463. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
  464. if err != nil {
  465. return errors.Wrap(err, "failed to get user access tokens")
  466. }
  467. userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
  468. AccessToken: accessToken,
  469. Description: description,
  470. }
  471. userAccessTokens = append(userAccessTokens, &userAccessToken)
  472. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  473. UserId: user.ID,
  474. Key: storepb.UserSettingKey_ACCESS_TOKENS,
  475. Value: &storepb.UserSetting_AccessTokens{
  476. AccessTokens: &storepb.AccessTokensUserSetting{
  477. AccessTokens: userAccessTokens,
  478. },
  479. },
  480. }); err != nil {
  481. return errors.Wrap(err, "failed to upsert user setting")
  482. }
  483. return nil
  484. }
  485. func convertUserFromStore(user *store.User) *v1pb.User {
  486. userpb := &v1pb.User{
  487. Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
  488. State: convertStateFromStore(user.RowStatus),
  489. CreateTime: timestamppb.New(time.Unix(user.CreatedTs, 0)),
  490. UpdateTime: timestamppb.New(time.Unix(user.UpdatedTs, 0)),
  491. Role: convertUserRoleFromStore(user.Role),
  492. Username: user.Username,
  493. Email: user.Email,
  494. Nickname: user.Nickname,
  495. AvatarUrl: user.AvatarURL,
  496. Description: user.Description,
  497. }
  498. // Use the avatar URL instead of raw base64 image data to reduce the response size.
  499. if user.AvatarURL != "" {
  500. userpb.AvatarUrl = fmt.Sprintf("/file/%s/avatar", userpb.Name)
  501. }
  502. return userpb
  503. }
  504. func convertUserRoleFromStore(role store.Role) v1pb.User_Role {
  505. switch role {
  506. case store.RoleHost:
  507. return v1pb.User_HOST
  508. case store.RoleAdmin:
  509. return v1pb.User_ADMIN
  510. case store.RoleUser:
  511. return v1pb.User_USER
  512. default:
  513. return v1pb.User_ROLE_UNSPECIFIED
  514. }
  515. }
  516. func convertUserRoleToStore(role v1pb.User_Role) store.Role {
  517. switch role {
  518. case v1pb.User_HOST:
  519. return store.RoleHost
  520. case v1pb.User_ADMIN:
  521. return store.RoleAdmin
  522. case v1pb.User_USER:
  523. return store.RoleUser
  524. default:
  525. return store.RoleUser
  526. }
  527. }
  528. func extractImageInfo(dataURI string) (string, string, error) {
  529. dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
  530. matches := dataURIRegex.FindStringSubmatch(dataURI)
  531. if len(matches) != 3 {
  532. return "", "", errors.New("Invalid data URI format")
  533. }
  534. imageType := matches[1]
  535. base64Data := matches[2]
  536. return imageType, base64Data, nil
  537. }