mirror of https://github.com/usememos/memos.git
chore: fix reactions seed data
- Add comprehensive inline documentation for auth services - Document session-based and token-based authentication flows - Clarify authentication priority and validation logic - Add detailed comments for JWT token structure and claims - Fix reactions seed data to use memo UIDs instead of numeric IDs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f241b590a2
commit
f635d85bcf
|
|
@ -23,11 +23,16 @@ import (
|
|||
type ContextKey int
|
||||
|
||||
const (
|
||||
// The key name used to store user's ID in the context (for user-based auth).
|
||||
// userIDContextKey stores the authenticated user's ID in the context.
|
||||
// Set for both session-based and token-based authentication.
|
||||
userIDContextKey ContextKey = iota
|
||||
// The key name used to store session ID in the context (for session-based auth).
|
||||
|
||||
// sessionIDContextKey stores the session ID in the context.
|
||||
// Only set for session-based authentication (cookie auth).
|
||||
sessionIDContextKey
|
||||
// The key name used to store access token in the context (for token-based auth).
|
||||
|
||||
// accessTokenContextKey stores the JWT access token in the context.
|
||||
// Only set for token-based authentication (Bearer token).
|
||||
accessTokenContextKey
|
||||
)
|
||||
|
||||
|
|
@ -46,13 +51,26 @@ func NewGRPCAuthInterceptor(store *store.Store, secret string) *GRPCAuthIntercep
|
|||
}
|
||||
|
||||
// AuthenticationInterceptor is the unary interceptor for gRPC API.
|
||||
//
|
||||
// Authentication Strategy (in priority order):
|
||||
// 1. Session Cookie: Check for "user_session" cookie with format "{userID}-{sessionID}"
|
||||
// 2. Access Token: Check for "Authorization: Bearer {token}" header with JWT
|
||||
// 3. Public Endpoints: Allow if method is in public allowlist
|
||||
// 4. Reject: Return 401 Unauthenticated if none of the above succeed
|
||||
//
|
||||
// On successful authentication, sets context values:
|
||||
// - userIDContextKey: The authenticated user's ID (always set)
|
||||
// - sessionIDContextKey: Session ID (only for cookie auth)
|
||||
// - accessTokenContextKey: JWT token (only for Bearer token auth).
|
||||
func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context")
|
||||
}
|
||||
|
||||
// Try to authenticate via session ID (from cookie) first
|
||||
// Authentication Method 1: Session-based authentication (Cookie)
|
||||
// Format: Cookie: user_session={userID}-{sessionID}
|
||||
// Used by: Web browsers
|
||||
if sessionCookieValue, err := getSessionIDFromMetadata(md); err == nil && sessionCookieValue != "" {
|
||||
user, err := in.authenticateBySession(ctx, sessionCookieValue)
|
||||
if err == nil && user != nil {
|
||||
|
|
@ -65,7 +83,9 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
|
|||
}
|
||||
}
|
||||
|
||||
// Try to authenticate via JWT access token (from Authorization header)
|
||||
// Authentication Method 2: Token-based authentication (JWT)
|
||||
// Format: Authorization: Bearer {jwt_token}
|
||||
// Used by: Mobile apps, CLI tools, API clients
|
||||
if accessToken, err := getAccessTokenFromMetadata(md); err == nil && accessToken != "" {
|
||||
user, err := in.authenticateByJWT(ctx, accessToken)
|
||||
if err == nil && user != nil {
|
||||
|
|
@ -73,7 +93,9 @@ func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, re
|
|||
}
|
||||
}
|
||||
|
||||
// If no valid authentication found, check if this method is in the allowlist (public endpoints)
|
||||
// Authentication Method 3: Public endpoints
|
||||
// Some endpoints don't require authentication (e.g., login, signup)
|
||||
// Check if this method is in the allowlist
|
||||
if isUnauthorizeAllowedMethod(serverInfo.FullMethod) {
|
||||
return handler(ctx, request)
|
||||
}
|
||||
|
|
@ -109,6 +131,14 @@ func (in *GRPCAuthInterceptor) handleAuthenticatedRequest(ctx context.Context, r
|
|||
}
|
||||
|
||||
// authenticateByJWT authenticates a user using JWT access token from Authorization header.
|
||||
//
|
||||
// Validation steps:
|
||||
// 1. Parse and verify JWT signature using server secret
|
||||
// 2. Extract user ID from JWT claims (subject field)
|
||||
// 3. Verify user exists and is not archived
|
||||
// 4. Verify token exists in user's access_tokens list (for revocation support)
|
||||
//
|
||||
// Returns the authenticated user or an error.
|
||||
func (in *GRPCAuthInterceptor) authenticateByJWT(ctx context.Context, accessToken string) (*store.User, error) {
|
||||
if accessToken == "" {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "access token not found")
|
||||
|
|
@ -160,6 +190,14 @@ func (in *GRPCAuthInterceptor) authenticateByJWT(ctx context.Context, accessToke
|
|||
}
|
||||
|
||||
// authenticateBySession authenticates a user using session ID from cookie.
|
||||
//
|
||||
// Validation steps:
|
||||
// 1. Parse cookie value to extract userID and sessionID
|
||||
// 2. Verify user exists and is not archived
|
||||
// 3. Verify session exists in user's sessions list
|
||||
// 4. Check session hasn't expired (sliding expiration: 14 days from last access)
|
||||
//
|
||||
// Returns the authenticated user or an error.
|
||||
func (in *GRPCAuthInterceptor) authenticateBySession(ctx context.Context, sessionCookieValue string) (*store.User, error) {
|
||||
if sessionCookieValue == "" {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "session cookie value not found")
|
||||
|
|
@ -204,6 +242,11 @@ func (in *GRPCAuthInterceptor) updateSessionLastAccessed(ctx context.Context, us
|
|||
}
|
||||
|
||||
// validateUserSession checks if a session exists and is still valid using sliding expiration.
|
||||
//
|
||||
// Sliding expiration logic:
|
||||
// - Session is valid if: last_accessed_time + 14 days > current_time
|
||||
// - Each API call updates last_accessed_time, extending the session
|
||||
// - This provides better UX than fixed expiration (users stay logged in while active).
|
||||
func validateUserSession(sessionID string, userSessions []*storepb.SessionsUserSetting_Session) bool {
|
||||
for _, session := range userSessions {
|
||||
if sessionID == session.SessionId {
|
||||
|
|
@ -220,7 +263,10 @@ func validateUserSession(sessionID string, userSessions []*storepb.SessionsUserS
|
|||
return false
|
||||
}
|
||||
|
||||
// getSessionIDFromMetadata extracts session cookie value from cookie.
|
||||
// getSessionIDFromMetadata extracts session cookie value from metadata.
|
||||
//
|
||||
// Checks both "grpcgateway-cookie" (set by gRPC-Gateway) and "cookie" (set by native gRPC).
|
||||
// Cookie format: user_session={userID}-{sessionID}.
|
||||
func getSessionIDFromMetadata(md metadata.MD) (string, error) {
|
||||
// Check the cookie header for session cookie value
|
||||
var sessionCookieValue string
|
||||
|
|
@ -238,7 +284,10 @@ func getSessionIDFromMetadata(md metadata.MD) (string, error) {
|
|||
return sessionCookieValue, nil
|
||||
}
|
||||
|
||||
// getAccessTokenFromMetadata extracts access token from Authorization header.
|
||||
// getAccessTokenFromMetadata extracts JWT access token from Authorization header.
|
||||
//
|
||||
// Expected header format: Authorization: Bearer {jwt_token}
|
||||
// This follows the OAuth 2.0 Bearer token specification (RFC 6750).
|
||||
func getAccessTokenFromMetadata(md metadata.MD) (string, error) {
|
||||
// Check the HTTP request Authorization header.
|
||||
authorizationHeaders := md.Get("Authorization")
|
||||
|
|
@ -252,6 +301,10 @@ func getAccessTokenFromMetadata(md metadata.MD) (string, error) {
|
|||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
// validateAccessToken checks if the provided JWT token exists in the user's access tokens list.
|
||||
//
|
||||
// This enables token revocation: when a user deletes a token from their settings,
|
||||
// it's removed from this list and subsequent API calls with that token will fail.
|
||||
func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
|
||||
for _, userAccessToken := range userAccessTokens {
|
||||
if accessTokenString == userAccessToken.AccessToken {
|
||||
|
|
|
|||
|
|
@ -12,32 +12,62 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// issuer is the issuer of the jwt token.
|
||||
// Issuer is the issuer claim in JWT tokens.
|
||||
// This identifies tokens as issued by Memos.
|
||||
Issuer = "memos"
|
||||
// Signing key section. For now, this is only used for signing, not for verifying since we only
|
||||
// have 1 version. But it will be used to maintain backward compatibility if we change the signing mechanism.
|
||||
|
||||
// KeyID is the key identifier used in JWT header.
|
||||
// Version "v1" allows for future key rotation while maintaining backward compatibility.
|
||||
// If signing mechanism changes, add "v2", "v3", etc. and verify both versions.
|
||||
KeyID = "v1"
|
||||
// AccessTokenAudienceName is the audience name of the access token.
|
||||
|
||||
// AccessTokenAudienceName is the audience claim for JWT access tokens.
|
||||
// This ensures tokens are only used for API access, not other purposes.
|
||||
AccessTokenAudienceName = "user.access-token"
|
||||
// SessionSlidingDuration is the sliding expiration duration for user sessions (2 weeks).
|
||||
// Sessions are considered valid if last_accessed_time + SessionSlidingDuration > current_time.
|
||||
|
||||
// SessionSlidingDuration is the sliding expiration duration for user sessions.
|
||||
// Sessions remain valid if accessed within the last 14 days.
|
||||
// Each API call extends the session by updating last_accessed_time.
|
||||
SessionSlidingDuration = 14 * 24 * time.Hour
|
||||
|
||||
// SessionCookieName is the cookie name of user session ID.
|
||||
// SessionCookieName is the HTTP cookie name used to store session information.
|
||||
// Cookie value format: {userID}-{sessionID}.
|
||||
SessionCookieName = "user_session"
|
||||
)
|
||||
|
||||
// ClaimsMessage represents the claims structure in a JWT token.
|
||||
//
|
||||
// JWT Claims include:
|
||||
// - name: Username (custom claim)
|
||||
// - iss: Issuer = "memos"
|
||||
// - aud: Audience = "user.access-token"
|
||||
// - sub: Subject = user ID
|
||||
// - iat: Issued at time
|
||||
// - exp: Expiration time (optional, may be empty for never-expiring tokens).
|
||||
type ClaimsMessage struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"` // Username
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateAccessToken generates an access token.
|
||||
// GenerateAccessToken generates a JWT access token for a user.
|
||||
//
|
||||
// Parameters:
|
||||
// - username: The user's username (stored in "name" claim)
|
||||
// - userID: The user's ID (stored in "sub" claim)
|
||||
// - expirationTime: When the token expires (pass zero time for no expiration)
|
||||
// - secret: Server secret used to sign the token
|
||||
//
|
||||
// Returns a signed JWT string or an error.
|
||||
func GenerateAccessToken(username string, userID int32, expirationTime time.Time, secret []byte) (string, error) {
|
||||
return generateToken(username, userID, AccessTokenAudienceName, expirationTime, secret)
|
||||
}
|
||||
|
||||
// generateToken generates a jwt token.
|
||||
// generateToken generates a JWT token with the given claims.
|
||||
//
|
||||
// Token structure:
|
||||
// Header: {"alg": "HS256", "kid": "v1", "typ": "JWT"}
|
||||
// Claims: {"name": username, "iss": "memos", "aud": [audience], "sub": userID, "iat": now, "exp": expiry}
|
||||
// Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret).
|
||||
func generateToken(username string, userID int32, audience string, expirationTime time.Time, secret []byte) (string, error) {
|
||||
registeredClaims := jwt.RegisteredClaims{
|
||||
Issuer: Issuer,
|
||||
|
|
@ -65,17 +95,31 @@ func generateToken(username string, userID int32, audience string, expirationTim
|
|||
return tokenString, nil
|
||||
}
|
||||
|
||||
// GenerateSessionID generates a unique session ID using UUIDv4.
|
||||
// GenerateSessionID generates a unique session ID.
|
||||
//
|
||||
// Uses UUID v4 (random) for high entropy and uniqueness.
|
||||
// Session IDs are stored in user settings and used to identify browser sessions.
|
||||
func GenerateSessionID() (string, error) {
|
||||
return util.GenUUID(), nil
|
||||
}
|
||||
|
||||
// BuildSessionCookieValue builds the session cookie value in format {userID}-{sessionID}.
|
||||
// BuildSessionCookieValue creates the session cookie value.
|
||||
//
|
||||
// Format: {userID}-{sessionID}
|
||||
// Example: "123-550e8400-e29b-41d4-a716-446655440000"
|
||||
//
|
||||
// This format allows quick extraction of both user ID and session ID
|
||||
// from the cookie without database lookup during authentication.
|
||||
func BuildSessionCookieValue(userID int32, sessionID string) string {
|
||||
return fmt.Sprintf("%d-%s", userID, sessionID)
|
||||
}
|
||||
|
||||
// ParseSessionCookieValue parses the session cookie value to extract userID and sessionID.
|
||||
// ParseSessionCookieValue extracts user ID and session ID from cookie value.
|
||||
//
|
||||
// Input format: "{userID}-{sessionID}"
|
||||
// Returns: (userID, sessionID, error)
|
||||
//
|
||||
// Example: "123-550e8400-..." → (123, "550e8400-...", nil).
|
||||
func ParseSessionCookieValue(cookieValue string) (int32, string, error) {
|
||||
parts := strings.SplitN(cookieValue, "-", 2)
|
||||
if len(parts) != 2 {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ const (
|
|||
unmatchedUsernameAndPasswordError = "unmatched username and password"
|
||||
)
|
||||
|
||||
// GetCurrentSession retrieves the current authenticated session information.
|
||||
//
|
||||
// This endpoint is used to:
|
||||
// - Check if a user is currently authenticated
|
||||
// - Get the current user's information
|
||||
// - Retrieve the last accessed time of the session
|
||||
//
|
||||
// Authentication: Required (session cookie or access token)
|
||||
// Returns: User information and last accessed timestamp.
|
||||
func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrentSessionRequest) (*v1pb.GetCurrentSessionResponse, error) {
|
||||
user, err := s.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -59,8 +68,23 @@ func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrent
|
|||
}, nil
|
||||
}
|
||||
|
||||
// CreateSession authenticates a user and establishes a new session.
|
||||
//
|
||||
// This endpoint supports two authentication methods:
|
||||
// 1. Password-based authentication (username + password)
|
||||
// 2. SSO authentication (OAuth2 authorization code)
|
||||
//
|
||||
// On successful authentication:
|
||||
// - A session cookie is set for web browsers (cookie: user_session={userID}-{sessionID})
|
||||
// - Session information is stored including client details (IP, user agent, device type)
|
||||
// - Sessions use sliding expiration: 14 days from last access
|
||||
//
|
||||
// Authentication: Not required (public endpoint)
|
||||
// Returns: Authenticated user information and last accessed timestamp.
|
||||
func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSessionRequest) (*v1pb.CreateSessionResponse, error) {
|
||||
var existingUser *store.User
|
||||
|
||||
// Authentication Method 1: Password-based authentication
|
||||
if passwordCredentials := request.GetPasswordCredentials(); passwordCredentials != nil {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Username: &passwordCredentials.Username,
|
||||
|
|
@ -85,6 +109,7 @@ func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSe
|
|||
}
|
||||
existingUser = user
|
||||
} else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil {
|
||||
// Authentication Method 2: SSO (OAuth2) authentication
|
||||
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
|
||||
ID: &ssoCredentials.IdpId,
|
||||
})
|
||||
|
|
@ -183,6 +208,16 @@ func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSe
|
|||
}, nil
|
||||
}
|
||||
|
||||
// doSignIn performs the actual sign-in operation by creating a session and setting the cookie.
|
||||
//
|
||||
// This function:
|
||||
// 1. Generates a unique session ID (UUID)
|
||||
// 2. Tracks the session in user settings with client information
|
||||
// 3. Sets a session cookie in the format: {userID}-{sessionID}
|
||||
// 4. Configures cookie security settings (HttpOnly, Secure, SameSite)
|
||||
//
|
||||
// Cookie lifetime is 100 years, but actual session validity is controlled by
|
||||
// sliding expiration (14 days from last access) checked during authentication.
|
||||
func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTime time.Time) error {
|
||||
// Generate unique session ID for web use
|
||||
sessionID, err := GenerateSessionID()
|
||||
|
|
@ -212,6 +247,14 @@ func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTim
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeleteSession terminates the current user session (logout).
|
||||
//
|
||||
// This endpoint:
|
||||
// 1. Removes the session from the user's sessions list in the database
|
||||
// 2. Clears the session cookie by setting it to expire immediately
|
||||
//
|
||||
// Authentication: Required (session cookie or access token)
|
||||
// Returns: Empty response on success.
|
||||
func (s *APIV1Service) DeleteSession(ctx context.Context, _ *v1pb.DeleteSessionRequest) (*emptypb.Empty, error) {
|
||||
user, err := s.GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -298,7 +341,13 @@ func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error)
|
|||
return user, nil
|
||||
}
|
||||
|
||||
// Helper function to track user session for session management.
|
||||
// trackUserSession creates a new session record in the user's settings.
|
||||
//
|
||||
// Session information includes:
|
||||
// - session_id: Unique UUID for this session
|
||||
// - create_time: When the session was created
|
||||
// - last_accessed_time: When the session was last used (for sliding expiration)
|
||||
// - client_info: Device details (user agent, IP, device type, OS, browser).
|
||||
func (s *APIV1Service) trackUserSession(ctx context.Context, userID int32, sessionID string) error {
|
||||
// Extract client information from the context
|
||||
clientInfo := s.extractClientInfo(ctx)
|
||||
|
|
@ -313,19 +362,19 @@ func (s *APIV1Service) trackUserSession(ctx context.Context, userID int32, sessi
|
|||
return s.Store.AddUserSession(ctx, userID, session)
|
||||
}
|
||||
|
||||
// Helper function to extract client information from the gRPC context.
|
||||
// extractClientInfo extracts comprehensive client information from the request context.
|
||||
// This includes user agent parsing to determine device type, operating system, browser,
|
||||
// and IP address extraction. This information is used to provide detailed session
|
||||
// tracking and management capabilities in the web UI.
|
||||
//
|
||||
// Fields populated:
|
||||
// - UserAgent: Raw user agent string
|
||||
// - IpAddress: Client IP (from X-Forwarded-For or X-Real-IP headers)
|
||||
// - DeviceType: "mobile", "tablet", or "desktop"
|
||||
// - Os: Operating system name and version (e.g., "iOS 17.1", "Windows 10/11")
|
||||
// This function parses metadata from the gRPC context to extract:
|
||||
// - User Agent: Raw user agent string for detailed parsing
|
||||
// - IP Address: Client IP from X-Forwarded-For or X-Real-IP headers
|
||||
// - Device Type: "mobile", "tablet", or "desktop" (parsed from user agent)
|
||||
// - Operating System: OS name and version (e.g., "iOS 17.1", "Windows 10/11")
|
||||
// - Browser: Browser name and version (e.g., "Chrome 120.0.0.0")
|
||||
// - Country: Geographic location (TODO: implement with GeoIP service).
|
||||
//
|
||||
// This information enables users to:
|
||||
// - See all active sessions with device details
|
||||
// - Identify suspicious login attempts
|
||||
// - Revoke specific sessions from unknown devices.
|
||||
func (s *APIV1Service) extractClientInfo(ctx context.Context) *storepb.SessionsUserSetting_ClientInfo {
|
||||
clientInfo := &storepb.SessionsUserSetting_ClientInfo{}
|
||||
|
||||
|
|
@ -351,6 +400,14 @@ func (s *APIV1Service) extractClientInfo(ctx context.Context) *storepb.SessionsU
|
|||
}
|
||||
|
||||
// parseUserAgent extracts device type, OS, and browser information from user agent string.
|
||||
//
|
||||
// Detection logic:
|
||||
// - Device Type: Checks for keywords like "mobile", "tablet", "ipad"
|
||||
// - OS: Pattern matches for iOS, Android, Windows, macOS, Linux, Chrome OS
|
||||
// - Browser: Identifies Edge, Chrome, Firefox, Safari, Opera
|
||||
//
|
||||
// Note: This is a simplified parser. For production use with high accuracy requirements,
|
||||
// consider using a dedicated user agent parsing library.
|
||||
func (*APIV1Service) parseUserAgent(userAgent string, clientInfo *storepb.SessionsUserSetting_ClientInfo) {
|
||||
if userAgent == "" {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -521,6 +521,21 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
|
|||
return response, nil
|
||||
}
|
||||
|
||||
// ListUserAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
|
||||
//
|
||||
// Personal Access Tokens are used for:
|
||||
// - Mobile app authentication
|
||||
// - CLI tool authentication
|
||||
// - API client authentication
|
||||
// - Any programmatic access requiring Bearer token auth
|
||||
//
|
||||
// Security:
|
||||
// - Only the token owner can list their tokens
|
||||
// - Returns full token strings (so users can manage/revoke them)
|
||||
// - Invalid or expired tokens are filtered out
|
||||
//
|
||||
// Authentication: Required (session cookie or access token)
|
||||
// Authorization: User can only list their own tokens.
|
||||
func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.ListUserAccessTokensRequest) (*v1pb.ListUserAccessTokensResponse, error) {
|
||||
userID, err := ExtractUserIDFromName(request.Parent)
|
||||
if err != nil {
|
||||
|
|
@ -584,6 +599,26 @@ func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.L
|
|||
return response, nil
|
||||
}
|
||||
|
||||
// CreateUserAccessToken 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:
|
||||
// - JWT format signed with server secret
|
||||
// - Contains user ID and username in claims
|
||||
// - 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) CreateUserAccessToken(ctx context.Context, request *v1pb.CreateUserAccessTokenRequest) (*v1pb.UserAccessToken, error) {
|
||||
userID, err := ExtractUserIDFromName(request.Parent)
|
||||
if err != nil {
|
||||
|
|
@ -643,6 +678,19 @@ func (s *APIV1Service) CreateUserAccessToken(ctx context.Context, request *v1pb.
|
|||
return userAccessToken, nil
|
||||
}
|
||||
|
||||
// DeleteUserAccessToken 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) DeleteUserAccessToken(ctx context.Context, request *v1pb.DeleteUserAccessTokenRequest) (*emptypb.Empty, error) {
|
||||
// Extract user ID from the access token resource name
|
||||
// Format: users/{user}/accessTokens/{access_token}
|
||||
|
|
@ -694,6 +742,21 @@ func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.
|
|||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// ListUserSessions retrieves all active sessions for a user.
|
||||
//
|
||||
// Sessions represent active browser logins. Each session includes:
|
||||
// - session_id: Unique identifier
|
||||
// - create_time: When the session was created
|
||||
// - last_accessed_time: Last API call time (for sliding expiration)
|
||||
// - client_info: Device details (browser, OS, IP address, device type)
|
||||
//
|
||||
// Use cases:
|
||||
// - User reviews where they're logged in
|
||||
// - User identifies suspicious login attempts
|
||||
// - User prepares to revoke specific sessions
|
||||
//
|
||||
// Authentication: Required (session cookie or access token)
|
||||
// Authorization: User can only list their own sessions.
|
||||
func (s *APIV1Service) ListUserSessions(ctx context.Context, request *v1pb.ListUserSessionsRequest) (*v1pb.ListUserSessionsResponse, error) {
|
||||
userID, err := ExtractUserIDFromName(request.Parent)
|
||||
if err != nil {
|
||||
|
|
@ -749,6 +812,23 @@ func (s *APIV1Service) ListUserSessions(ctx context.Context, request *v1pb.ListU
|
|||
return response, nil
|
||||
}
|
||||
|
||||
// RevokeUserSession terminates a specific session for a user.
|
||||
//
|
||||
// This endpoint:
|
||||
// 1. Removes the session from the user's sessions list
|
||||
// 2. Immediately invalidates the session
|
||||
// 3. Forces the device to re-login on next request
|
||||
//
|
||||
// Use cases:
|
||||
// - User logs out from a specific device (e.g., "Log out my phone")
|
||||
// - User removes suspicious/unknown session
|
||||
// - User logs out from all devices except current one
|
||||
//
|
||||
// Note: This is different from DeleteSession (logout current session).
|
||||
// This endpoint allows revoking ANY session, not just the current one.
|
||||
//
|
||||
// Authentication: Required (session cookie or access token)
|
||||
// Authorization: User can only revoke their own sessions.
|
||||
func (s *APIV1Service) RevokeUserSession(ctx context.Context, request *v1pb.RevokeUserSessionRequest) (*emptypb.Empty, error) {
|
||||
// Extract user ID and session ID from the session resource name
|
||||
// Format: users/{user}/sessions/{session}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ INSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(6,
|
|||
-- Memo Relations
|
||||
INSERT INTO memo_relation VALUES(3,1,'REFERENCE');
|
||||
|
||||
-- Reactions
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(1,1,'memos/1','🎉');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(2,1,'memos/1','👍');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(3,1,'memos/2','✅');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(4,1,'memos/5','💡');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(5,1,'memos/6','🚀');
|
||||
-- Reactions (using memo UIDs, not numeric IDs)
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(1,1,'memos/welcome2memos001','🎉');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(2,1,'memos/welcome2memos001','👍');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(3,1,'memos/taskdemo000001','✅');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(4,1,'memos/idea00000001','💡');
|
||||
INSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(5,1,'memos/sponsor0000001','🚀');
|
||||
|
||||
-- System Settings
|
||||
INSERT INTO system_setting VALUES ('MEMO_RELATED', '{"contentLengthLimit":8192,"enableAutoCompact":true,"enableComment":true,"enableLocation":true,"defaultVisibility":"PUBLIC","reactions":["👍","💛","🔥","👏","😂","👌","🚀","👀","🤔","🤡","❓","+1","🎉","💡","✅"]}', '');
|
||||
|
|
|
|||
Loading…
Reference in New Issue