memos/server/router/api/v1/user_service_access_tokens.go
boojack 3fe145083f chore: reorganize backend and frontend modules
- remove the unused internal cron package
- split API service implementations by responsibility
- clarify frontend shared-module ownership
2026-07-12 17:59:12 +08:00

152 lines
5.1 KiB
Go

package v1
import (
"context"
"fmt"
"strings"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/internal/util"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/auth"
)
func (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1pb.ListPersonalAccessTokensRequest) (*v1pb.ListPersonalAccessTokensResponse, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
userID := user.ID
// Verify permission
if _, err := s.authorizeUserResourceAccess(ctx, userID, true); err != nil {
return nil, err
}
tokens, err := s.Store.GetUserPersonalAccessTokens(ctx, userID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get access tokens: %v", err)
}
personalAccessTokens := make([]*v1pb.PersonalAccessToken, len(tokens))
for i, token := range tokens {
personalAccessTokens[i] = &v1pb.PersonalAccessToken{
Name: fmt.Sprintf("%s/personalAccessTokens/%s", BuildUserName(user.Username), token.TokenId),
Description: token.Description,
ExpiresAt: token.ExpiresAt,
CreatedAt: token.CreatedAt,
LastUsedAt: token.LastUsedAt,
}
}
return &v1pb.ListPersonalAccessTokensResponse{PersonalAccessTokens: personalAccessTokens}, nil
}
// CreatePersonalAccessToken creates a new Personal Access Token (PAT) for a user.
//
// Use cases:
// - User manually creates token in settings for mobile app
// - User creates token for CLI tool
// - User creates token for third-party integration
//
// Token properties:
// - Random string with memos_pat_ prefix
// - SHA-256 hash stored in database
// - Optional expiration time (can be never-expiring)
// - User-provided description for identification
//
// Security considerations:
// - Full token is only shown ONCE (in this response)
// - User should copy and store it securely
// - Token can be revoked by deleting it from settings
//
// Authentication: Required (session cookie or access token)
// Authorization: User can only create tokens for themselves.
func (s *APIV1Service) CreatePersonalAccessToken(ctx context.Context, request *v1pb.CreatePersonalAccessTokenRequest) (*v1pb.CreatePersonalAccessTokenResponse, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
userID := user.ID
// Verify permission
if _, err := s.authorizeUserResourceAccess(ctx, userID, false); err != nil {
return nil, err
}
// Generate PAT
tokenID := util.GenUUID()
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
var expiresAt *timestamppb.Timestamp
if request.ExpiresInDays > 0 {
expiresAt = timestamppb.New(time.Now().AddDate(0, 0, int(request.ExpiresInDays)))
}
patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: request.Description,
ExpiresAt: expiresAt,
CreatedAt: timestamppb.Now(),
}
if err := s.Store.AddUserPersonalAccessToken(ctx, userID, patRecord); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err)
}
return &v1pb.CreatePersonalAccessTokenResponse{
PersonalAccessToken: &v1pb.PersonalAccessToken{
Name: fmt.Sprintf("%s/personalAccessTokens/%s", BuildUserName(user.Username), tokenID),
Description: request.Description,
ExpiresAt: expiresAt,
CreatedAt: patRecord.CreatedAt,
},
Token: token, // Only returned on creation
}, nil
}
// DeletePersonalAccessToken revokes a Personal Access Token.
//
// This endpoint:
// 1. Removes the token from the user's access tokens list
// 2. Immediately invalidates the token (subsequent API calls with it will fail)
//
// Use cases:
// - User revokes a compromised token
// - User removes token for unused app/device
// - User cleans up old tokens
//
// Authentication: Required (session cookie or access token)
// Authorization: User can only delete their own tokens.
func (s *APIV1Service) DeletePersonalAccessToken(ctx context.Context, request *v1pb.DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {
parts := strings.Split(request.Name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "personalAccessTokens" {
return nil, status.Errorf(codes.InvalidArgument, "invalid personal access token name")
}
user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
userID := user.ID
tokenID := parts[3]
// Verify permission
if _, err := s.authorizeUserResourceAccess(ctx, userID, false); err != nil {
return nil, err
}
if err := s.Store.RemoveUserPersonalAccessToken(ctx, userID, tokenID); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete access token: %v", err)
}
return &emptypb.Empty{}, nil
}