refactor: user auth improvements (#5360)

This commit is contained in:
Johnny 2025-12-18 18:15:51 +08:00 committed by GitHub
parent 2c2ef53737
commit 7932f6d0d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 4739 additions and 2757 deletions

4
.gitignore vendored
View File

@ -7,6 +7,10 @@ web/dist
# Build artifacts
build/
bin/
memos
# Plan/design documents
docs/plans/
.DS_Store

View File

@ -11,87 +11,104 @@ import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v1";
service AuthService {
// GetCurrentSession returns the current active session information.
// This method is idempotent and safe, suitable for checking current session state.
rpc GetCurrentSession(GetCurrentSessionRequest) returns (GetCurrentSessionResponse) {
option (google.api.http) = {get: "/api/v1/auth/sessions/current"};
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
// Similar to OIDC's /userinfo endpoint.
rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) {
option (google.api.http) = {get: "/api/v1/auth/me"};
}
// CreateSession authenticates a user and creates a new session.
// Returns the authenticated user information upon successful authentication.
rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) {
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
// Supports password-based and SSO authentication methods.
rpc SignIn(SignInRequest) returns (SignInResponse) {
option (google.api.http) = {
post: "/api/v1/auth/sessions"
post: "/api/v1/auth/signin"
body: "*"
};
}
// DeleteSession terminates the current user session.
// This is an idempotent operation that invalidates the user's authentication.
rpc DeleteSession(DeleteSessionRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/auth/sessions/current"};
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
rpc SignOut(SignOutRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {post: "/api/v1/auth/signout"};
}
// RefreshToken exchanges a valid refresh token for a new access token.
// The refresh token is read from the HttpOnly cookie.
// Returns a new short-lived access token.
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse) {
option (google.api.http) = {
post: "/api/v1/auth/refresh"
body: "*"
};
}
}
message GetCurrentSessionRequest {}
message GetCurrentUserRequest {}
message GetCurrentSessionResponse {
message GetCurrentUserResponse {
// The authenticated user's information.
User user = 1;
// Last time the session was accessed.
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
google.protobuf.Timestamp last_accessed_at = 2;
}
message CreateSessionRequest {
message SignInRequest {
// Nested message for password-based authentication credentials.
message PasswordCredentials {
// The username to sign in with.
// Required field for password-based authentication.
string username = 1 [(google.api.field_behavior) = REQUIRED];
// The password to sign in with.
// Required field for password-based authentication.
string password = 2 [(google.api.field_behavior) = REQUIRED];
}
// Nested message for SSO authentication credentials.
message SSOCredentials {
// The ID of the SSO provider.
// Required field to identify the SSO provider.
int32 idp_id = 1 [(google.api.field_behavior) = REQUIRED];
// The authorization code from the SSO provider.
// Required field for completing the SSO flow.
string code = 2 [(google.api.field_behavior) = REQUIRED];
// The redirect URI used in the SSO flow.
// Required field for security validation.
string redirect_uri = 3 [(google.api.field_behavior) = REQUIRED];
// The PKCE code verifier for enhanced security (RFC 7636).
// Optional field - if provided, enables PKCE flow protection against authorization code interception.
// Optional - enables PKCE flow protection against authorization code interception.
string code_verifier = 4 [(google.api.field_behavior) = OPTIONAL];
}
// Provide one authentication method (username/password or SSO).
// Required field to specify the authentication method.
// Authentication credentials. Provide one method.
oneof credentials {
// Username and password authentication method.
// Username and password authentication.
PasswordCredentials password_credentials = 1;
// SSO provider authentication method.
// SSO provider authentication.
SSOCredentials sso_credentials = 2;
}
}
message CreateSessionResponse {
// The authenticated user information.
message SignInResponse {
// The authenticated user's information.
User user = 1;
// Last time the session was accessed.
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
google.protobuf.Timestamp last_accessed_at = 2;
// The short-lived access token for API requests.
// Store in memory only, not in localStorage.
string access_token = 2;
// When the access token expires.
// Client should call RefreshToken before this time.
google.protobuf.Timestamp access_token_expires_at = 3;
}
message DeleteSessionRequest {}
message SignOutRequest {}
message RefreshTokenRequest {}
message RefreshTokenResponse {
// The new short-lived access token.
string access_token = 1;
// When the access token expires.
google.protobuf.Timestamp expires_at = 2;
}

View File

@ -84,35 +84,39 @@ service UserService {
option (google.api.method_signature) = "parent";
}
// ListUserAccessTokens returns a list of access tokens for a user.
rpc ListUserAccessTokens(ListUserAccessTokensRequest) returns (ListUserAccessTokensResponse) {
option (google.api.http) = {get: "/api/v1/{parent=users/*}/accessTokens"};
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
option (google.api.http) = {get: "/api/v1/{parent=users/*}/personalAccessTokens"};
option (google.api.method_signature) = "parent";
}
// CreateUserAccessToken creates a new access token for a user.
rpc CreateUserAccessToken(CreateUserAccessTokenRequest) returns (UserAccessToken) {
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
// The token value is only returned once upon creation.
rpc CreatePersonalAccessToken(CreatePersonalAccessTokenRequest) returns (CreatePersonalAccessTokenResponse) {
option (google.api.http) = {
post: "/api/v1/{parent=users/*}/accessTokens"
body: "access_token"
post: "/api/v1/{parent=users/*}/personalAccessTokens"
body: "*"
};
option (google.api.method_signature) = "parent,access_token";
}
// DeleteUserAccessToken deletes an access token.
rpc DeleteUserAccessToken(DeleteUserAccessTokenRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=users/*/accessTokens/*}"};
// DeletePersonalAccessToken deletes a Personal Access Token.
rpc DeletePersonalAccessToken(DeletePersonalAccessTokenRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=users/*/personalAccessTokens/*}"};
option (google.api.method_signature) = "name";
}
// ListUserSessions returns a list of active sessions for a user.
rpc ListUserSessions(ListUserSessionsRequest) returns (ListUserSessionsResponse) {
// ListSessions returns a list of active login sessions for a user.
// Each session represents a browser/device where the user is logged in.
// Sessions are backed by refresh tokens with sliding expiration.
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) {
option (google.api.http) = {get: "/api/v1/{parent=users/*}/sessions"};
option (google.api.method_signature) = "parent";
}
// RevokeUserSession revokes a specific session for a user.
rpc RevokeUserSession(RevokeUserSessionRequest) returns (google.protobuf.Empty) {
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
rpc RevokeSession(RevokeSessionRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=users/*/sessions/*}"};
option (google.api.method_signature) = "name";
}
@ -398,9 +402,9 @@ message UserSetting {
KEY_UNSPECIFIED = 0;
// GENERAL is the key for general user settings.
GENERAL = 1;
// SESSIONS is the key for user authentication sessions.
// SESSIONS is the key for user login sessions (refresh tokens).
SESSIONS = 2;
// ACCESS_TOKENS is the key for access tokens.
// ACCESS_TOKENS is the key for Personal Access Tokens (PATs).
ACCESS_TOKENS = 3;
// WEBHOOKS is the key for user webhooks.
WEBHOOKS = 4;
@ -420,14 +424,14 @@ message UserSetting {
// User authentication sessions configuration.
message SessionsSetting {
// List of active user sessions.
repeated UserSession sessions = 1;
// List of active login sessions.
repeated Session sessions = 1;
}
// User access tokens configuration.
// Personal access tokens configuration.
message AccessTokensSetting {
// List of user access tokens.
repeated UserAccessToken access_tokens = 1;
// List of personal access tokens (PATs).
repeated PersonalAccessToken personal_access_tokens = 1;
}
// User webhooks configuration.
@ -487,85 +491,97 @@ message ListUserSettingsResponse {
int32 total_size = 3;
}
// User access token message
message UserAccessToken {
// PersonalAccessToken represents a long-lived token for API/script access.
// PATs are distinct from short-lived JWT access tokens used for session authentication.
message PersonalAccessToken {
option (google.api.resource) = {
type: "memos.api.v1/UserAccessToken"
pattern: "users/{user}/accessTokens/{access_token}"
singular: "userAccessToken"
plural: "userAccessTokens"
type: "memos.api.v1/PersonalAccessToken"
pattern: "users/{user}/personalAccessTokens/{personal_access_token}"
singular: "personalAccessToken"
plural: "personalAccessTokens"
};
// The resource name of the access token.
// Format: users/{user}/accessTokens/{access_token}
// The resource name of the personal access token.
// Format: users/{user}/personalAccessTokens/{personal_access_token}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// Output only. The access token value.
string access_token = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
// The description of the token.
string description = 2 [(google.api.field_behavior) = OPTIONAL];
// The description of the access token.
string description = 3 [(google.api.field_behavior) = OPTIONAL];
// Output only. The issued timestamp.
google.protobuf.Timestamp issued_at = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
// Output only. The creation timestamp.
google.protobuf.Timestamp created_at = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
// Optional. The expiration timestamp.
google.protobuf.Timestamp expires_at = 5 [(google.api.field_behavior) = OPTIONAL];
google.protobuf.Timestamp expires_at = 4 [(google.api.field_behavior) = OPTIONAL];
// Output only. The last used timestamp.
google.protobuf.Timestamp last_used_at = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message ListUserAccessTokensRequest {
// Required. The parent resource whose access tokens will be listed.
message ListPersonalAccessTokensRequest {
// Required. The parent resource whose personal access tokens will be listed.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// Optional. The maximum number of access tokens to return.
// Optional. The maximum number of tokens to return.
int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. A page token for pagination.
string page_token = 3 [(google.api.field_behavior) = OPTIONAL];
}
message ListUserAccessTokensResponse {
// The list of access tokens.
repeated UserAccessToken access_tokens = 1;
message ListPersonalAccessTokensResponse {
// The list of personal access tokens.
repeated PersonalAccessToken personal_access_tokens = 1;
// A token for the next page of results.
string next_page_token = 2;
// The total count of access tokens.
// The total count of personal access tokens.
int32 total_size = 3;
}
message CreateUserAccessTokenRequest {
// Required. The parent resource where this access token will be created.
message CreatePersonalAccessTokenRequest {
// Required. The parent resource where this token will be created.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// Required. The access token to create.
UserAccessToken access_token = 2 [(google.api.field_behavior) = REQUIRED];
// Optional. Description of the personal access token.
string description = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. The access token ID to use.
string access_token_id = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. Expiration duration in days (0 = never expires).
int32 expires_in_days = 3 [(google.api.field_behavior) = OPTIONAL];
}
message DeleteUserAccessTokenRequest {
// Required. The resource name of the access token to delete.
// Format: users/{user}/accessTokens/{access_token}
message CreatePersonalAccessTokenResponse {
// The personal access token metadata.
PersonalAccessToken personal_access_token = 1;
// The actual token value - only returned on creation.
// This is the only time the token value will be visible.
string token = 2;
}
message DeletePersonalAccessTokenRequest {
// Required. The resource name of the personal access token to delete.
// Format: users/{user}/personalAccessTokens/{personal_access_token}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/UserAccessToken"}
(google.api.resource_reference) = {type: "memos.api.v1/PersonalAccessToken"}
];
}
message UserSession {
// Session represents a user's login session on a specific device/browser.
// Sessions are backed by refresh tokens with sliding expiration.
message Session {
option (google.api.resource) = {
type: "memos.api.v1/UserSession"
type: "memos.api.v1/Session"
pattern: "users/{user}/sessions/{session}"
name_field: "name"
};
@ -605,7 +621,7 @@ message UserSession {
}
}
message ListUserSessionsRequest {
message ListSessionsRequest {
// Required. The resource name of the parent.
// Format: users/{user}
string parent = 1 [
@ -614,12 +630,12 @@ message ListUserSessionsRequest {
];
}
message ListUserSessionsResponse {
// The list of user sessions.
repeated UserSession sessions = 1;
message ListSessionsResponse {
// The list of sessions.
repeated Session sessions = 1;
}
message RevokeUserSessionRequest {
message RevokeSessionRequest {
// The name of the session to revoke.
// Format: users/{user}/sessions/{session}
string name = 1 [(google.api.field_behavior) = REQUIRED];

View File

@ -34,28 +34,35 @@ const (
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// AuthServiceGetCurrentSessionProcedure is the fully-qualified name of the AuthService's
// GetCurrentSession RPC.
AuthServiceGetCurrentSessionProcedure = "/memos.api.v1.AuthService/GetCurrentSession"
// AuthServiceCreateSessionProcedure is the fully-qualified name of the AuthService's CreateSession
// AuthServiceGetCurrentUserProcedure is the fully-qualified name of the AuthService's
// GetCurrentUser RPC.
AuthServiceGetCurrentUserProcedure = "/memos.api.v1.AuthService/GetCurrentUser"
// AuthServiceSignInProcedure is the fully-qualified name of the AuthService's SignIn RPC.
AuthServiceSignInProcedure = "/memos.api.v1.AuthService/SignIn"
// AuthServiceSignOutProcedure is the fully-qualified name of the AuthService's SignOut RPC.
AuthServiceSignOutProcedure = "/memos.api.v1.AuthService/SignOut"
// AuthServiceRefreshTokenProcedure is the fully-qualified name of the AuthService's RefreshToken
// RPC.
AuthServiceCreateSessionProcedure = "/memos.api.v1.AuthService/CreateSession"
// AuthServiceDeleteSessionProcedure is the fully-qualified name of the AuthService's DeleteSession
// RPC.
AuthServiceDeleteSessionProcedure = "/memos.api.v1.AuthService/DeleteSession"
AuthServiceRefreshTokenProcedure = "/memos.api.v1.AuthService/RefreshToken"
)
// AuthServiceClient is a client for the memos.api.v1.AuthService service.
type AuthServiceClient interface {
// GetCurrentSession returns the current active session information.
// This method is idempotent and safe, suitable for checking current session state.
GetCurrentSession(context.Context, *connect.Request[v1.GetCurrentSessionRequest]) (*connect.Response[v1.GetCurrentSessionResponse], error)
// CreateSession authenticates a user and creates a new session.
// Returns the authenticated user information upon successful authentication.
CreateSession(context.Context, *connect.Request[v1.CreateSessionRequest]) (*connect.Response[v1.CreateSessionResponse], error)
// DeleteSession terminates the current user session.
// This is an idempotent operation that invalidates the user's authentication.
DeleteSession(context.Context, *connect.Request[v1.DeleteSessionRequest]) (*connect.Response[emptypb.Empty], error)
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
// Similar to OIDC's /userinfo endpoint.
GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error)
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
// Supports password-based and SSO authentication methods.
SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error)
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error)
// RefreshToken exchanges a valid refresh token for a new access token.
// The refresh token is read from the HttpOnly cookie.
// Returns a new short-lived access token.
RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)
}
// NewAuthServiceClient constructs a client for the memos.api.v1.AuthService service. By default, it
@ -69,22 +76,28 @@ func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
baseURL = strings.TrimRight(baseURL, "/")
authServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName("AuthService").Methods()
return &authServiceClient{
getCurrentSession: connect.NewClient[v1.GetCurrentSessionRequest, v1.GetCurrentSessionResponse](
getCurrentUser: connect.NewClient[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse](
httpClient,
baseURL+AuthServiceGetCurrentSessionProcedure,
connect.WithSchema(authServiceMethods.ByName("GetCurrentSession")),
baseURL+AuthServiceGetCurrentUserProcedure,
connect.WithSchema(authServiceMethods.ByName("GetCurrentUser")),
connect.WithClientOptions(opts...),
),
createSession: connect.NewClient[v1.CreateSessionRequest, v1.CreateSessionResponse](
signIn: connect.NewClient[v1.SignInRequest, v1.SignInResponse](
httpClient,
baseURL+AuthServiceCreateSessionProcedure,
connect.WithSchema(authServiceMethods.ByName("CreateSession")),
baseURL+AuthServiceSignInProcedure,
connect.WithSchema(authServiceMethods.ByName("SignIn")),
connect.WithClientOptions(opts...),
),
deleteSession: connect.NewClient[v1.DeleteSessionRequest, emptypb.Empty](
signOut: connect.NewClient[v1.SignOutRequest, emptypb.Empty](
httpClient,
baseURL+AuthServiceDeleteSessionProcedure,
connect.WithSchema(authServiceMethods.ByName("DeleteSession")),
baseURL+AuthServiceSignOutProcedure,
connect.WithSchema(authServiceMethods.ByName("SignOut")),
connect.WithClientOptions(opts...),
),
refreshToken: connect.NewClient[v1.RefreshTokenRequest, v1.RefreshTokenResponse](
httpClient,
baseURL+AuthServiceRefreshTokenProcedure,
connect.WithSchema(authServiceMethods.ByName("RefreshToken")),
connect.WithClientOptions(opts...),
),
}
@ -92,37 +105,49 @@ func NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
// authServiceClient implements AuthServiceClient.
type authServiceClient struct {
getCurrentSession *connect.Client[v1.GetCurrentSessionRequest, v1.GetCurrentSessionResponse]
createSession *connect.Client[v1.CreateSessionRequest, v1.CreateSessionResponse]
deleteSession *connect.Client[v1.DeleteSessionRequest, emptypb.Empty]
getCurrentUser *connect.Client[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse]
signIn *connect.Client[v1.SignInRequest, v1.SignInResponse]
signOut *connect.Client[v1.SignOutRequest, emptypb.Empty]
refreshToken *connect.Client[v1.RefreshTokenRequest, v1.RefreshTokenResponse]
}
// GetCurrentSession calls memos.api.v1.AuthService.GetCurrentSession.
func (c *authServiceClient) GetCurrentSession(ctx context.Context, req *connect.Request[v1.GetCurrentSessionRequest]) (*connect.Response[v1.GetCurrentSessionResponse], error) {
return c.getCurrentSession.CallUnary(ctx, req)
// GetCurrentUser calls memos.api.v1.AuthService.GetCurrentUser.
func (c *authServiceClient) GetCurrentUser(ctx context.Context, req *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) {
return c.getCurrentUser.CallUnary(ctx, req)
}
// CreateSession calls memos.api.v1.AuthService.CreateSession.
func (c *authServiceClient) CreateSession(ctx context.Context, req *connect.Request[v1.CreateSessionRequest]) (*connect.Response[v1.CreateSessionResponse], error) {
return c.createSession.CallUnary(ctx, req)
// SignIn calls memos.api.v1.AuthService.SignIn.
func (c *authServiceClient) SignIn(ctx context.Context, req *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) {
return c.signIn.CallUnary(ctx, req)
}
// DeleteSession calls memos.api.v1.AuthService.DeleteSession.
func (c *authServiceClient) DeleteSession(ctx context.Context, req *connect.Request[v1.DeleteSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return c.deleteSession.CallUnary(ctx, req)
// SignOut calls memos.api.v1.AuthService.SignOut.
func (c *authServiceClient) SignOut(ctx context.Context, req *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {
return c.signOut.CallUnary(ctx, req)
}
// RefreshToken calls memos.api.v1.AuthService.RefreshToken.
func (c *authServiceClient) RefreshToken(ctx context.Context, req *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {
return c.refreshToken.CallUnary(ctx, req)
}
// AuthServiceHandler is an implementation of the memos.api.v1.AuthService service.
type AuthServiceHandler interface {
// GetCurrentSession returns the current active session information.
// This method is idempotent and safe, suitable for checking current session state.
GetCurrentSession(context.Context, *connect.Request[v1.GetCurrentSessionRequest]) (*connect.Response[v1.GetCurrentSessionResponse], error)
// CreateSession authenticates a user and creates a new session.
// Returns the authenticated user information upon successful authentication.
CreateSession(context.Context, *connect.Request[v1.CreateSessionRequest]) (*connect.Response[v1.CreateSessionResponse], error)
// DeleteSession terminates the current user session.
// This is an idempotent operation that invalidates the user's authentication.
DeleteSession(context.Context, *connect.Request[v1.DeleteSessionRequest]) (*connect.Response[emptypb.Empty], error)
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
// Similar to OIDC's /userinfo endpoint.
GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error)
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
// Supports password-based and SSO authentication methods.
SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error)
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error)
// RefreshToken exchanges a valid refresh token for a new access token.
// The refresh token is read from the HttpOnly cookie.
// Returns a new short-lived access token.
RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)
}
// NewAuthServiceHandler builds an HTTP handler from the service implementation. It returns the path
@ -132,32 +157,40 @@ type AuthServiceHandler interface {
// and JSON codecs. They also support gzip compression.
func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
authServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName("AuthService").Methods()
authServiceGetCurrentSessionHandler := connect.NewUnaryHandler(
AuthServiceGetCurrentSessionProcedure,
svc.GetCurrentSession,
connect.WithSchema(authServiceMethods.ByName("GetCurrentSession")),
authServiceGetCurrentUserHandler := connect.NewUnaryHandler(
AuthServiceGetCurrentUserProcedure,
svc.GetCurrentUser,
connect.WithSchema(authServiceMethods.ByName("GetCurrentUser")),
connect.WithHandlerOptions(opts...),
)
authServiceCreateSessionHandler := connect.NewUnaryHandler(
AuthServiceCreateSessionProcedure,
svc.CreateSession,
connect.WithSchema(authServiceMethods.ByName("CreateSession")),
authServiceSignInHandler := connect.NewUnaryHandler(
AuthServiceSignInProcedure,
svc.SignIn,
connect.WithSchema(authServiceMethods.ByName("SignIn")),
connect.WithHandlerOptions(opts...),
)
authServiceDeleteSessionHandler := connect.NewUnaryHandler(
AuthServiceDeleteSessionProcedure,
svc.DeleteSession,
connect.WithSchema(authServiceMethods.ByName("DeleteSession")),
authServiceSignOutHandler := connect.NewUnaryHandler(
AuthServiceSignOutProcedure,
svc.SignOut,
connect.WithSchema(authServiceMethods.ByName("SignOut")),
connect.WithHandlerOptions(opts...),
)
authServiceRefreshTokenHandler := connect.NewUnaryHandler(
AuthServiceRefreshTokenProcedure,
svc.RefreshToken,
connect.WithSchema(authServiceMethods.ByName("RefreshToken")),
connect.WithHandlerOptions(opts...),
)
return "/memos.api.v1.AuthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case AuthServiceGetCurrentSessionProcedure:
authServiceGetCurrentSessionHandler.ServeHTTP(w, r)
case AuthServiceCreateSessionProcedure:
authServiceCreateSessionHandler.ServeHTTP(w, r)
case AuthServiceDeleteSessionProcedure:
authServiceDeleteSessionHandler.ServeHTTP(w, r)
case AuthServiceGetCurrentUserProcedure:
authServiceGetCurrentUserHandler.ServeHTTP(w, r)
case AuthServiceSignInProcedure:
authServiceSignInHandler.ServeHTTP(w, r)
case AuthServiceSignOutProcedure:
authServiceSignOutHandler.ServeHTTP(w, r)
case AuthServiceRefreshTokenProcedure:
authServiceRefreshTokenHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -167,14 +200,18 @@ func NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption
// UnimplementedAuthServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedAuthServiceHandler struct{}
func (UnimplementedAuthServiceHandler) GetCurrentSession(context.Context, *connect.Request[v1.GetCurrentSessionRequest]) (*connect.Response[v1.GetCurrentSessionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.GetCurrentSession is not implemented"))
func (UnimplementedAuthServiceHandler) GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.GetCurrentUser is not implemented"))
}
func (UnimplementedAuthServiceHandler) CreateSession(context.Context, *connect.Request[v1.CreateSessionRequest]) (*connect.Response[v1.CreateSessionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.CreateSession is not implemented"))
func (UnimplementedAuthServiceHandler) SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.SignIn is not implemented"))
}
func (UnimplementedAuthServiceHandler) DeleteSession(context.Context, *connect.Request[v1.DeleteSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.DeleteSession is not implemented"))
func (UnimplementedAuthServiceHandler) SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.SignOut is not implemented"))
}
func (UnimplementedAuthServiceHandler) RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.AuthService.RefreshToken is not implemented"))
}

View File

@ -59,21 +59,21 @@ const (
// UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's
// ListUserSettings RPC.
UserServiceListUserSettingsProcedure = "/memos.api.v1.UserService/ListUserSettings"
// UserServiceListUserAccessTokensProcedure is the fully-qualified name of the UserService's
// ListUserAccessTokens RPC.
UserServiceListUserAccessTokensProcedure = "/memos.api.v1.UserService/ListUserAccessTokens"
// UserServiceCreateUserAccessTokenProcedure is the fully-qualified name of the UserService's
// CreateUserAccessToken RPC.
UserServiceCreateUserAccessTokenProcedure = "/memos.api.v1.UserService/CreateUserAccessToken"
// UserServiceDeleteUserAccessTokenProcedure is the fully-qualified name of the UserService's
// DeleteUserAccessToken RPC.
UserServiceDeleteUserAccessTokenProcedure = "/memos.api.v1.UserService/DeleteUserAccessToken"
// UserServiceListUserSessionsProcedure is the fully-qualified name of the UserService's
// ListUserSessions RPC.
UserServiceListUserSessionsProcedure = "/memos.api.v1.UserService/ListUserSessions"
// UserServiceRevokeUserSessionProcedure is the fully-qualified name of the UserService's
// RevokeUserSession RPC.
UserServiceRevokeUserSessionProcedure = "/memos.api.v1.UserService/RevokeUserSession"
// UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's
// ListPersonalAccessTokens RPC.
UserServiceListPersonalAccessTokensProcedure = "/memos.api.v1.UserService/ListPersonalAccessTokens"
// UserServiceCreatePersonalAccessTokenProcedure is the fully-qualified name of the UserService's
// CreatePersonalAccessToken RPC.
UserServiceCreatePersonalAccessTokenProcedure = "/memos.api.v1.UserService/CreatePersonalAccessToken"
// UserServiceDeletePersonalAccessTokenProcedure is the fully-qualified name of the UserService's
// DeletePersonalAccessToken RPC.
UserServiceDeletePersonalAccessTokenProcedure = "/memos.api.v1.UserService/DeletePersonalAccessToken"
// UserServiceListSessionsProcedure is the fully-qualified name of the UserService's ListSessions
// RPC.
UserServiceListSessionsProcedure = "/memos.api.v1.UserService/ListSessions"
// UserServiceRevokeSessionProcedure is the fully-qualified name of the UserService's RevokeSession
// RPC.
UserServiceRevokeSessionProcedure = "/memos.api.v1.UserService/RevokeSession"
// UserServiceListUserWebhooksProcedure is the fully-qualified name of the UserService's
// ListUserWebhooks RPC.
UserServiceListUserWebhooksProcedure = "/memos.api.v1.UserService/ListUserWebhooks"
@ -122,16 +122,21 @@ type UserServiceClient interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListUserAccessTokens returns a list of access tokens for a user.
ListUserAccessTokens(context.Context, *connect.Request[v1.ListUserAccessTokensRequest]) (*connect.Response[v1.ListUserAccessTokensResponse], error)
// CreateUserAccessToken creates a new access token for a user.
CreateUserAccessToken(context.Context, *connect.Request[v1.CreateUserAccessTokenRequest]) (*connect.Response[v1.UserAccessToken], error)
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken(context.Context, *connect.Request[v1.DeleteUserAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions(context.Context, *connect.Request[v1.ListUserSessionsRequest]) (*connect.Response[v1.ListUserSessionsResponse], error)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession(context.Context, *connect.Request[v1.RevokeUserSessionRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
// The token value is only returned once upon creation.
CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error)
// DeletePersonalAccessToken deletes a Personal Access Token.
DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)
// ListSessions returns a list of active login sessions for a user.
// Each session represents a browser/device where the user is logged in.
// Sessions are backed by refresh tokens with sliding expiration.
ListSessions(context.Context, *connect.Request[v1.ListSessionsRequest]) (*connect.Response[v1.ListSessionsResponse], error)
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
RevokeSession(context.Context, *connect.Request[v1.RevokeSessionRequest]) (*connect.Response[emptypb.Empty], error)
// ListUserWebhooks returns a list of webhooks for a user.
ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error)
// CreateUserWebhook creates a new webhook for a user.
@ -219,34 +224,34 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithClientOptions(opts...),
),
listUserAccessTokens: connect.NewClient[v1.ListUserAccessTokensRequest, v1.ListUserAccessTokensResponse](
listPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse](
httpClient,
baseURL+UserServiceListUserAccessTokensProcedure,
connect.WithSchema(userServiceMethods.ByName("ListUserAccessTokens")),
baseURL+UserServiceListPersonalAccessTokensProcedure,
connect.WithSchema(userServiceMethods.ByName("ListPersonalAccessTokens")),
connect.WithClientOptions(opts...),
),
createUserAccessToken: connect.NewClient[v1.CreateUserAccessTokenRequest, v1.UserAccessToken](
createPersonalAccessToken: connect.NewClient[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse](
httpClient,
baseURL+UserServiceCreateUserAccessTokenProcedure,
connect.WithSchema(userServiceMethods.ByName("CreateUserAccessToken")),
baseURL+UserServiceCreatePersonalAccessTokenProcedure,
connect.WithSchema(userServiceMethods.ByName("CreatePersonalAccessToken")),
connect.WithClientOptions(opts...),
),
deleteUserAccessToken: connect.NewClient[v1.DeleteUserAccessTokenRequest, emptypb.Empty](
deletePersonalAccessToken: connect.NewClient[v1.DeletePersonalAccessTokenRequest, emptypb.Empty](
httpClient,
baseURL+UserServiceDeleteUserAccessTokenProcedure,
connect.WithSchema(userServiceMethods.ByName("DeleteUserAccessToken")),
baseURL+UserServiceDeletePersonalAccessTokenProcedure,
connect.WithSchema(userServiceMethods.ByName("DeletePersonalAccessToken")),
connect.WithClientOptions(opts...),
),
listUserSessions: connect.NewClient[v1.ListUserSessionsRequest, v1.ListUserSessionsResponse](
listSessions: connect.NewClient[v1.ListSessionsRequest, v1.ListSessionsResponse](
httpClient,
baseURL+UserServiceListUserSessionsProcedure,
connect.WithSchema(userServiceMethods.ByName("ListUserSessions")),
baseURL+UserServiceListSessionsProcedure,
connect.WithSchema(userServiceMethods.ByName("ListSessions")),
connect.WithClientOptions(opts...),
),
revokeUserSession: connect.NewClient[v1.RevokeUserSessionRequest, emptypb.Empty](
revokeSession: connect.NewClient[v1.RevokeSessionRequest, emptypb.Empty](
httpClient,
baseURL+UserServiceRevokeUserSessionProcedure,
connect.WithSchema(userServiceMethods.ByName("RevokeUserSession")),
baseURL+UserServiceRevokeSessionProcedure,
connect.WithSchema(userServiceMethods.ByName("RevokeSession")),
connect.WithClientOptions(opts...),
),
listUserWebhooks: connect.NewClient[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse](
@ -296,28 +301,28 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
// userServiceClient implements UserServiceClient.
type userServiceClient struct {
listUsers *connect.Client[v1.ListUsersRequest, v1.ListUsersResponse]
getUser *connect.Client[v1.GetUserRequest, v1.User]
createUser *connect.Client[v1.CreateUserRequest, v1.User]
updateUser *connect.Client[v1.UpdateUserRequest, v1.User]
deleteUser *connect.Client[v1.DeleteUserRequest, emptypb.Empty]
listAllUserStats *connect.Client[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse]
getUserStats *connect.Client[v1.GetUserStatsRequest, v1.UserStats]
getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting]
updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting]
listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]
listUserAccessTokens *connect.Client[v1.ListUserAccessTokensRequest, v1.ListUserAccessTokensResponse]
createUserAccessToken *connect.Client[v1.CreateUserAccessTokenRequest, v1.UserAccessToken]
deleteUserAccessToken *connect.Client[v1.DeleteUserAccessTokenRequest, emptypb.Empty]
listUserSessions *connect.Client[v1.ListUserSessionsRequest, v1.ListUserSessionsResponse]
revokeUserSession *connect.Client[v1.RevokeUserSessionRequest, emptypb.Empty]
listUserWebhooks *connect.Client[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse]
createUserWebhook *connect.Client[v1.CreateUserWebhookRequest, v1.UserWebhook]
updateUserWebhook *connect.Client[v1.UpdateUserWebhookRequest, v1.UserWebhook]
deleteUserWebhook *connect.Client[v1.DeleteUserWebhookRequest, emptypb.Empty]
listUserNotifications *connect.Client[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse]
updateUserNotification *connect.Client[v1.UpdateUserNotificationRequest, v1.UserNotification]
deleteUserNotification *connect.Client[v1.DeleteUserNotificationRequest, emptypb.Empty]
listUsers *connect.Client[v1.ListUsersRequest, v1.ListUsersResponse]
getUser *connect.Client[v1.GetUserRequest, v1.User]
createUser *connect.Client[v1.CreateUserRequest, v1.User]
updateUser *connect.Client[v1.UpdateUserRequest, v1.User]
deleteUser *connect.Client[v1.DeleteUserRequest, emptypb.Empty]
listAllUserStats *connect.Client[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse]
getUserStats *connect.Client[v1.GetUserStatsRequest, v1.UserStats]
getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting]
updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting]
listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]
listPersonalAccessTokens *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]
createPersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse]
deletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty]
listSessions *connect.Client[v1.ListSessionsRequest, v1.ListSessionsResponse]
revokeSession *connect.Client[v1.RevokeSessionRequest, emptypb.Empty]
listUserWebhooks *connect.Client[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse]
createUserWebhook *connect.Client[v1.CreateUserWebhookRequest, v1.UserWebhook]
updateUserWebhook *connect.Client[v1.UpdateUserWebhookRequest, v1.UserWebhook]
deleteUserWebhook *connect.Client[v1.DeleteUserWebhookRequest, emptypb.Empty]
listUserNotifications *connect.Client[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse]
updateUserNotification *connect.Client[v1.UpdateUserNotificationRequest, v1.UserNotification]
deleteUserNotification *connect.Client[v1.DeleteUserNotificationRequest, emptypb.Empty]
}
// ListUsers calls memos.api.v1.UserService.ListUsers.
@ -370,29 +375,29 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.R
return c.listUserSettings.CallUnary(ctx, req)
}
// ListUserAccessTokens calls memos.api.v1.UserService.ListUserAccessTokens.
func (c *userServiceClient) ListUserAccessTokens(ctx context.Context, req *connect.Request[v1.ListUserAccessTokensRequest]) (*connect.Response[v1.ListUserAccessTokensResponse], error) {
return c.listUserAccessTokens.CallUnary(ctx, req)
// ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens.
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return c.listPersonalAccessTokens.CallUnary(ctx, req)
}
// CreateUserAccessToken calls memos.api.v1.UserService.CreateUserAccessToken.
func (c *userServiceClient) CreateUserAccessToken(ctx context.Context, req *connect.Request[v1.CreateUserAccessTokenRequest]) (*connect.Response[v1.UserAccessToken], error) {
return c.createUserAccessToken.CallUnary(ctx, req)
// CreatePersonalAccessToken calls memos.api.v1.UserService.CreatePersonalAccessToken.
func (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) {
return c.createPersonalAccessToken.CallUnary(ctx, req)
}
// DeleteUserAccessToken calls memos.api.v1.UserService.DeleteUserAccessToken.
func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, req *connect.Request[v1.DeleteUserAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
return c.deleteUserAccessToken.CallUnary(ctx, req)
// DeletePersonalAccessToken calls memos.api.v1.UserService.DeletePersonalAccessToken.
func (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
return c.deletePersonalAccessToken.CallUnary(ctx, req)
}
// ListUserSessions calls memos.api.v1.UserService.ListUserSessions.
func (c *userServiceClient) ListUserSessions(ctx context.Context, req *connect.Request[v1.ListUserSessionsRequest]) (*connect.Response[v1.ListUserSessionsResponse], error) {
return c.listUserSessions.CallUnary(ctx, req)
// ListSessions calls memos.api.v1.UserService.ListSessions.
func (c *userServiceClient) ListSessions(ctx context.Context, req *connect.Request[v1.ListSessionsRequest]) (*connect.Response[v1.ListSessionsResponse], error) {
return c.listSessions.CallUnary(ctx, req)
}
// RevokeUserSession calls memos.api.v1.UserService.RevokeUserSession.
func (c *userServiceClient) RevokeUserSession(ctx context.Context, req *connect.Request[v1.RevokeUserSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return c.revokeUserSession.CallUnary(ctx, req)
// RevokeSession calls memos.api.v1.UserService.RevokeSession.
func (c *userServiceClient) RevokeSession(ctx context.Context, req *connect.Request[v1.RevokeSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return c.revokeSession.CallUnary(ctx, req)
}
// ListUserWebhooks calls memos.api.v1.UserService.ListUserWebhooks.
@ -455,16 +460,21 @@ type UserServiceHandler interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListUserAccessTokens returns a list of access tokens for a user.
ListUserAccessTokens(context.Context, *connect.Request[v1.ListUserAccessTokensRequest]) (*connect.Response[v1.ListUserAccessTokensResponse], error)
// CreateUserAccessToken creates a new access token for a user.
CreateUserAccessToken(context.Context, *connect.Request[v1.CreateUserAccessTokenRequest]) (*connect.Response[v1.UserAccessToken], error)
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken(context.Context, *connect.Request[v1.DeleteUserAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions(context.Context, *connect.Request[v1.ListUserSessionsRequest]) (*connect.Response[v1.ListUserSessionsResponse], error)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession(context.Context, *connect.Request[v1.RevokeUserSessionRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
// The token value is only returned once upon creation.
CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error)
// DeletePersonalAccessToken deletes a Personal Access Token.
DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)
// ListSessions returns a list of active login sessions for a user.
// Each session represents a browser/device where the user is logged in.
// Sessions are backed by refresh tokens with sliding expiration.
ListSessions(context.Context, *connect.Request[v1.ListSessionsRequest]) (*connect.Response[v1.ListSessionsResponse], error)
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
RevokeSession(context.Context, *connect.Request[v1.RevokeSessionRequest]) (*connect.Response[emptypb.Empty], error)
// ListUserWebhooks returns a list of webhooks for a user.
ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error)
// CreateUserWebhook creates a new webhook for a user.
@ -548,34 +558,34 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithHandlerOptions(opts...),
)
userServiceListUserAccessTokensHandler := connect.NewUnaryHandler(
UserServiceListUserAccessTokensProcedure,
svc.ListUserAccessTokens,
connect.WithSchema(userServiceMethods.ByName("ListUserAccessTokens")),
userServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler(
UserServiceListPersonalAccessTokensProcedure,
svc.ListPersonalAccessTokens,
connect.WithSchema(userServiceMethods.ByName("ListPersonalAccessTokens")),
connect.WithHandlerOptions(opts...),
)
userServiceCreateUserAccessTokenHandler := connect.NewUnaryHandler(
UserServiceCreateUserAccessTokenProcedure,
svc.CreateUserAccessToken,
connect.WithSchema(userServiceMethods.ByName("CreateUserAccessToken")),
userServiceCreatePersonalAccessTokenHandler := connect.NewUnaryHandler(
UserServiceCreatePersonalAccessTokenProcedure,
svc.CreatePersonalAccessToken,
connect.WithSchema(userServiceMethods.ByName("CreatePersonalAccessToken")),
connect.WithHandlerOptions(opts...),
)
userServiceDeleteUserAccessTokenHandler := connect.NewUnaryHandler(
UserServiceDeleteUserAccessTokenProcedure,
svc.DeleteUserAccessToken,
connect.WithSchema(userServiceMethods.ByName("DeleteUserAccessToken")),
userServiceDeletePersonalAccessTokenHandler := connect.NewUnaryHandler(
UserServiceDeletePersonalAccessTokenProcedure,
svc.DeletePersonalAccessToken,
connect.WithSchema(userServiceMethods.ByName("DeletePersonalAccessToken")),
connect.WithHandlerOptions(opts...),
)
userServiceListUserSessionsHandler := connect.NewUnaryHandler(
UserServiceListUserSessionsProcedure,
svc.ListUserSessions,
connect.WithSchema(userServiceMethods.ByName("ListUserSessions")),
userServiceListSessionsHandler := connect.NewUnaryHandler(
UserServiceListSessionsProcedure,
svc.ListSessions,
connect.WithSchema(userServiceMethods.ByName("ListSessions")),
connect.WithHandlerOptions(opts...),
)
userServiceRevokeUserSessionHandler := connect.NewUnaryHandler(
UserServiceRevokeUserSessionProcedure,
svc.RevokeUserSession,
connect.WithSchema(userServiceMethods.ByName("RevokeUserSession")),
userServiceRevokeSessionHandler := connect.NewUnaryHandler(
UserServiceRevokeSessionProcedure,
svc.RevokeSession,
connect.WithSchema(userServiceMethods.ByName("RevokeSession")),
connect.WithHandlerOptions(opts...),
)
userServiceListUserWebhooksHandler := connect.NewUnaryHandler(
@ -642,16 +652,16 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
userServiceUpdateUserSettingHandler.ServeHTTP(w, r)
case UserServiceListUserSettingsProcedure:
userServiceListUserSettingsHandler.ServeHTTP(w, r)
case UserServiceListUserAccessTokensProcedure:
userServiceListUserAccessTokensHandler.ServeHTTP(w, r)
case UserServiceCreateUserAccessTokenProcedure:
userServiceCreateUserAccessTokenHandler.ServeHTTP(w, r)
case UserServiceDeleteUserAccessTokenProcedure:
userServiceDeleteUserAccessTokenHandler.ServeHTTP(w, r)
case UserServiceListUserSessionsProcedure:
userServiceListUserSessionsHandler.ServeHTTP(w, r)
case UserServiceRevokeUserSessionProcedure:
userServiceRevokeUserSessionHandler.ServeHTTP(w, r)
case UserServiceListPersonalAccessTokensProcedure:
userServiceListPersonalAccessTokensHandler.ServeHTTP(w, r)
case UserServiceCreatePersonalAccessTokenProcedure:
userServiceCreatePersonalAccessTokenHandler.ServeHTTP(w, r)
case UserServiceDeletePersonalAccessTokenProcedure:
userServiceDeletePersonalAccessTokenHandler.ServeHTTP(w, r)
case UserServiceListSessionsProcedure:
userServiceListSessionsHandler.ServeHTTP(w, r)
case UserServiceRevokeSessionProcedure:
userServiceRevokeSessionHandler.ServeHTTP(w, r)
case UserServiceListUserWebhooksProcedure:
userServiceListUserWebhooksHandler.ServeHTTP(w, r)
case UserServiceCreateUserWebhookProcedure:
@ -715,24 +725,24 @@ func (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connec
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSettings is not implemented"))
}
func (UnimplementedUserServiceHandler) ListUserAccessTokens(context.Context, *connect.Request[v1.ListUserAccessTokensRequest]) (*connect.Response[v1.ListUserAccessTokensResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserAccessTokens is not implemented"))
func (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListPersonalAccessTokens is not implemented"))
}
func (UnimplementedUserServiceHandler) CreateUserAccessToken(context.Context, *connect.Request[v1.CreateUserAccessTokenRequest]) (*connect.Response[v1.UserAccessToken], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreateUserAccessToken is not implemented"))
func (UnimplementedUserServiceHandler) CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreatePersonalAccessToken is not implemented"))
}
func (UnimplementedUserServiceHandler) DeleteUserAccessToken(context.Context, *connect.Request[v1.DeleteUserAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteUserAccessToken is not implemented"))
func (UnimplementedUserServiceHandler) DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeletePersonalAccessToken is not implemented"))
}
func (UnimplementedUserServiceHandler) ListUserSessions(context.Context, *connect.Request[v1.ListUserSessionsRequest]) (*connect.Response[v1.ListUserSessionsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSessions is not implemented"))
func (UnimplementedUserServiceHandler) ListSessions(context.Context, *connect.Request[v1.ListSessionsRequest]) (*connect.Response[v1.ListSessionsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListSessions is not implemented"))
}
func (UnimplementedUserServiceHandler) RevokeUserSession(context.Context, *connect.Request[v1.RevokeUserSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.RevokeUserSession is not implemented"))
func (UnimplementedUserServiceHandler) RevokeSession(context.Context, *connect.Request[v1.RevokeSessionRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.RevokeSession is not implemented"))
}
func (UnimplementedUserServiceHandler) ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) {

View File

@ -24,26 +24,26 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetCurrentSessionRequest struct {
type GetCurrentUserRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetCurrentSessionRequest) Reset() {
*x = GetCurrentSessionRequest{}
func (x *GetCurrentUserRequest) Reset() {
*x = GetCurrentUserRequest{}
mi := &file_api_v1_auth_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetCurrentSessionRequest) String() string {
func (x *GetCurrentUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetCurrentSessionRequest) ProtoMessage() {}
func (*GetCurrentUserRequest) ProtoMessage() {}
func (x *GetCurrentSessionRequest) ProtoReflect() protoreflect.Message {
func (x *GetCurrentUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -55,35 +55,33 @@ func (x *GetCurrentSessionRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetCurrentSessionRequest.ProtoReflect.Descriptor instead.
func (*GetCurrentSessionRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use GetCurrentUserRequest.ProtoReflect.Descriptor instead.
func (*GetCurrentUserRequest) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{0}
}
type GetCurrentSessionResponse struct {
type GetCurrentUserResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
// Last time the session was accessed.
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// The authenticated user's information.
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetCurrentSessionResponse) Reset() {
*x = GetCurrentSessionResponse{}
func (x *GetCurrentUserResponse) Reset() {
*x = GetCurrentUserResponse{}
mi := &file_api_v1_auth_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetCurrentSessionResponse) String() string {
func (x *GetCurrentUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetCurrentSessionResponse) ProtoMessage() {}
func (*GetCurrentUserResponse) ProtoMessage() {}
func (x *GetCurrentSessionResponse) ProtoReflect() protoreflect.Message {
func (x *GetCurrentUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -95,53 +93,45 @@ func (x *GetCurrentSessionResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetCurrentSessionResponse.ProtoReflect.Descriptor instead.
func (*GetCurrentSessionResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use GetCurrentUserResponse.ProtoReflect.Descriptor instead.
func (*GetCurrentUserResponse) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{1}
}
func (x *GetCurrentSessionResponse) GetUser() *User {
func (x *GetCurrentUserResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
func (x *GetCurrentSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp {
if x != nil {
return x.LastAccessedAt
}
return nil
}
type CreateSessionRequest struct {
type SignInRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Provide one authentication method (username/password or SSO).
// Required field to specify the authentication method.
// Authentication credentials. Provide one method.
//
// Types that are valid to be assigned to Credentials:
//
// *CreateSessionRequest_PasswordCredentials_
// *CreateSessionRequest_SsoCredentials
Credentials isCreateSessionRequest_Credentials `protobuf_oneof:"credentials"`
// *SignInRequest_PasswordCredentials_
// *SignInRequest_SsoCredentials
Credentials isSignInRequest_Credentials `protobuf_oneof:"credentials"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSessionRequest) Reset() {
*x = CreateSessionRequest{}
func (x *SignInRequest) Reset() {
*x = SignInRequest{}
mi := &file_api_v1_auth_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSessionRequest) String() string {
func (x *SignInRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSessionRequest) ProtoMessage() {}
func (*SignInRequest) ProtoMessage() {}
func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message {
func (x *SignInRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -153,79 +143,82 @@ func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use CreateSessionRequest.ProtoReflect.Descriptor instead.
func (*CreateSessionRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use SignInRequest.ProtoReflect.Descriptor instead.
func (*SignInRequest) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2}
}
func (x *CreateSessionRequest) GetCredentials() isCreateSessionRequest_Credentials {
func (x *SignInRequest) GetCredentials() isSignInRequest_Credentials {
if x != nil {
return x.Credentials
}
return nil
}
func (x *CreateSessionRequest) GetPasswordCredentials() *CreateSessionRequest_PasswordCredentials {
func (x *SignInRequest) GetPasswordCredentials() *SignInRequest_PasswordCredentials {
if x != nil {
if x, ok := x.Credentials.(*CreateSessionRequest_PasswordCredentials_); ok {
if x, ok := x.Credentials.(*SignInRequest_PasswordCredentials_); ok {
return x.PasswordCredentials
}
}
return nil
}
func (x *CreateSessionRequest) GetSsoCredentials() *CreateSessionRequest_SSOCredentials {
func (x *SignInRequest) GetSsoCredentials() *SignInRequest_SSOCredentials {
if x != nil {
if x, ok := x.Credentials.(*CreateSessionRequest_SsoCredentials); ok {
if x, ok := x.Credentials.(*SignInRequest_SsoCredentials); ok {
return x.SsoCredentials
}
}
return nil
}
type isCreateSessionRequest_Credentials interface {
isCreateSessionRequest_Credentials()
type isSignInRequest_Credentials interface {
isSignInRequest_Credentials()
}
type CreateSessionRequest_PasswordCredentials_ struct {
// Username and password authentication method.
PasswordCredentials *CreateSessionRequest_PasswordCredentials `protobuf:"bytes,1,opt,name=password_credentials,json=passwordCredentials,proto3,oneof"`
type SignInRequest_PasswordCredentials_ struct {
// Username and password authentication.
PasswordCredentials *SignInRequest_PasswordCredentials `protobuf:"bytes,1,opt,name=password_credentials,json=passwordCredentials,proto3,oneof"`
}
type CreateSessionRequest_SsoCredentials struct {
// SSO provider authentication method.
SsoCredentials *CreateSessionRequest_SSOCredentials `protobuf:"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof"`
type SignInRequest_SsoCredentials struct {
// SSO provider authentication.
SsoCredentials *SignInRequest_SSOCredentials `protobuf:"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof"`
}
func (*CreateSessionRequest_PasswordCredentials_) isCreateSessionRequest_Credentials() {}
func (*SignInRequest_PasswordCredentials_) isSignInRequest_Credentials() {}
func (*CreateSessionRequest_SsoCredentials) isCreateSessionRequest_Credentials() {}
func (*SignInRequest_SsoCredentials) isSignInRequest_Credentials() {}
type CreateSessionResponse struct {
type SignInResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The authenticated user information.
// The authenticated user's information.
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
// Last time the session was accessed.
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// The short-lived access token for API requests.
// Store in memory only, not in localStorage.
AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
// When the access token expires.
// Client should call RefreshToken before this time.
AccessTokenExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=access_token_expires_at,json=accessTokenExpiresAt,proto3" json:"access_token_expires_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSessionResponse) Reset() {
*x = CreateSessionResponse{}
func (x *SignInResponse) Reset() {
*x = SignInResponse{}
mi := &file_api_v1_auth_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSessionResponse) String() string {
func (x *SignInResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSessionResponse) ProtoMessage() {}
func (*SignInResponse) ProtoMessage() {}
func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message {
func (x *SignInResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -237,45 +230,52 @@ func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use CreateSessionResponse.ProtoReflect.Descriptor instead.
func (*CreateSessionResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use SignInResponse.ProtoReflect.Descriptor instead.
func (*SignInResponse) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{3}
}
func (x *CreateSessionResponse) GetUser() *User {
func (x *SignInResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
func (x *CreateSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp {
func (x *SignInResponse) GetAccessToken() string {
if x != nil {
return x.LastAccessedAt
return x.AccessToken
}
return ""
}
func (x *SignInResponse) GetAccessTokenExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.AccessTokenExpiresAt
}
return nil
}
type DeleteSessionRequest struct {
type SignOutRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteSessionRequest) Reset() {
*x = DeleteSessionRequest{}
func (x *SignOutRequest) Reset() {
*x = SignOutRequest{}
mi := &file_api_v1_auth_service_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteSessionRequest) String() string {
func (x *SignOutRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteSessionRequest) ProtoMessage() {}
func (*SignOutRequest) ProtoMessage() {}
func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message {
func (x *SignOutRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -287,39 +287,127 @@ func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use DeleteSessionRequest.ProtoReflect.Descriptor instead.
func (*DeleteSessionRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use SignOutRequest.ProtoReflect.Descriptor instead.
func (*SignOutRequest) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{4}
}
type RefreshTokenRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokenRequest) Reset() {
*x = RefreshTokenRequest{}
mi := &file_api_v1_auth_service_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RefreshTokenRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokenRequest) ProtoMessage() {}
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RefreshTokenRequest.ProtoReflect.Descriptor instead.
func (*RefreshTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{5}
}
type RefreshTokenResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The new short-lived access token.
AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
// When the access token expires.
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokenResponse) Reset() {
*x = RefreshTokenResponse{}
mi := &file_api_v1_auth_service_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RefreshTokenResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokenResponse) ProtoMessage() {}
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RefreshTokenResponse.ProtoReflect.Descriptor instead.
func (*RefreshTokenResponse) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{6}
}
func (x *RefreshTokenResponse) GetAccessToken() string {
if x != nil {
return x.AccessToken
}
return ""
}
func (x *RefreshTokenResponse) GetExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.ExpiresAt
}
return nil
}
// Nested message for password-based authentication credentials.
type CreateSessionRequest_PasswordCredentials struct {
type SignInRequest_PasswordCredentials struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The username to sign in with.
// Required field for password-based authentication.
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
// The password to sign in with.
// Required field for password-based authentication.
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSessionRequest_PasswordCredentials) Reset() {
*x = CreateSessionRequest_PasswordCredentials{}
mi := &file_api_v1_auth_service_proto_msgTypes[5]
func (x *SignInRequest_PasswordCredentials) Reset() {
*x = SignInRequest_PasswordCredentials{}
mi := &file_api_v1_auth_service_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSessionRequest_PasswordCredentials) String() string {
func (x *SignInRequest_PasswordCredentials) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSessionRequest_PasswordCredentials) ProtoMessage() {}
func (*SignInRequest_PasswordCredentials) ProtoMessage() {}
func (x *CreateSessionRequest_PasswordCredentials) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[5]
func (x *SignInRequest_PasswordCredentials) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -330,19 +418,19 @@ func (x *CreateSessionRequest_PasswordCredentials) ProtoReflect() protoreflect.M
return mi.MessageOf(x)
}
// Deprecated: Use CreateSessionRequest_PasswordCredentials.ProtoReflect.Descriptor instead.
func (*CreateSessionRequest_PasswordCredentials) Descriptor() ([]byte, []int) {
// Deprecated: Use SignInRequest_PasswordCredentials.ProtoReflect.Descriptor instead.
func (*SignInRequest_PasswordCredentials) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 0}
}
func (x *CreateSessionRequest_PasswordCredentials) GetUsername() string {
func (x *SignInRequest_PasswordCredentials) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *CreateSessionRequest_PasswordCredentials) GetPassword() string {
func (x *SignInRequest_PasswordCredentials) GetPassword() string {
if x != nil {
return x.Password
}
@ -350,39 +438,36 @@ func (x *CreateSessionRequest_PasswordCredentials) GetPassword() string {
}
// Nested message for SSO authentication credentials.
type CreateSessionRequest_SSOCredentials struct {
type SignInRequest_SSOCredentials struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The ID of the SSO provider.
// Required field to identify the SSO provider.
IdpId int32 `protobuf:"varint,1,opt,name=idp_id,json=idpId,proto3" json:"idp_id,omitempty"`
// The authorization code from the SSO provider.
// Required field for completing the SSO flow.
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
// The redirect URI used in the SSO flow.
// Required field for security validation.
RedirectUri string `protobuf:"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"`
// The PKCE code verifier for enhanced security (RFC 7636).
// Optional field - if provided, enables PKCE flow protection against authorization code interception.
// Optional - enables PKCE flow protection against authorization code interception.
CodeVerifier string `protobuf:"bytes,4,opt,name=code_verifier,json=codeVerifier,proto3" json:"code_verifier,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSessionRequest_SSOCredentials) Reset() {
*x = CreateSessionRequest_SSOCredentials{}
mi := &file_api_v1_auth_service_proto_msgTypes[6]
func (x *SignInRequest_SSOCredentials) Reset() {
*x = SignInRequest_SSOCredentials{}
mi := &file_api_v1_auth_service_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSessionRequest_SSOCredentials) String() string {
func (x *SignInRequest_SSOCredentials) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSessionRequest_SSOCredentials) ProtoMessage() {}
func (*SignInRequest_SSOCredentials) ProtoMessage() {}
func (x *CreateSessionRequest_SSOCredentials) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[6]
func (x *SignInRequest_SSOCredentials) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_auth_service_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -393,33 +478,33 @@ func (x *CreateSessionRequest_SSOCredentials) ProtoReflect() protoreflect.Messag
return mi.MessageOf(x)
}
// Deprecated: Use CreateSessionRequest_SSOCredentials.ProtoReflect.Descriptor instead.
func (*CreateSessionRequest_SSOCredentials) Descriptor() ([]byte, []int) {
// Deprecated: Use SignInRequest_SSOCredentials.ProtoReflect.Descriptor instead.
func (*SignInRequest_SSOCredentials) Descriptor() ([]byte, []int) {
return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 1}
}
func (x *CreateSessionRequest_SSOCredentials) GetIdpId() int32 {
func (x *SignInRequest_SSOCredentials) GetIdpId() int32 {
if x != nil {
return x.IdpId
}
return 0
}
func (x *CreateSessionRequest_SSOCredentials) GetCode() string {
func (x *SignInRequest_SSOCredentials) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *CreateSessionRequest_SSOCredentials) GetRedirectUri() string {
func (x *SignInRequest_SSOCredentials) GetRedirectUri() string {
if x != nil {
return x.RedirectUri
}
return ""
}
func (x *CreateSessionRequest_SSOCredentials) GetCodeVerifier() string {
func (x *SignInRequest_SSOCredentials) GetCodeVerifier() string {
if x != nil {
return x.CodeVerifier
}
@ -430,14 +515,13 @@ var File_api_v1_auth_service_proto protoreflect.FileDescriptor
const file_api_v1_auth_service_proto_rawDesc = "" +
"\n" +
"\x19api/v1/auth_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n" +
"\x18GetCurrentSessionRequest\"\x89\x01\n" +
"\x19GetCurrentSessionResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" +
"\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\xe3\x03\n" +
"\x14CreateSessionRequest\x12k\n" +
"\x14password_credentials\x18\x01 \x01(\v26.memos.api.v1.CreateSessionRequest.PasswordCredentialsH\x00R\x13passwordCredentials\x12\\\n" +
"\x0fsso_credentials\x18\x02 \x01(\v21.memos.api.v1.CreateSessionRequest.SSOCredentialsH\x00R\x0essoCredentials\x1aW\n" +
"\x19api/v1/auth_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n" +
"\x15GetCurrentUserRequest\"@\n" +
"\x16GetCurrentUserResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\"\xce\x03\n" +
"\rSignInRequest\x12d\n" +
"\x14password_credentials\x18\x01 \x01(\v2/.memos.api.v1.SignInRequest.PasswordCredentialsH\x00R\x13passwordCredentials\x12U\n" +
"\x0fsso_credentials\x18\x02 \x01(\v2*.memos.api.v1.SignInRequest.SSOCredentialsH\x00R\x0essoCredentials\x1aW\n" +
"\x13PasswordCredentials\x12\x1f\n" +
"\busername\x18\x01 \x01(\tB\x03\xe0A\x02R\busername\x12\x1f\n" +
"\bpassword\x18\x02 \x01(\tB\x03\xe0A\x02R\bpassword\x1a\x97\x01\n" +
@ -446,15 +530,22 @@ const file_api_v1_auth_service_proto_rawDesc = "" +
"\x04code\x18\x02 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" +
"\fredirect_uri\x18\x03 \x01(\tB\x03\xe0A\x02R\vredirectUri\x12(\n" +
"\rcode_verifier\x18\x04 \x01(\tB\x03\xe0A\x01R\fcodeVerifierB\r\n" +
"\vcredentials\"\x85\x01\n" +
"\x15CreateSessionResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" +
"\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\x16\n" +
"\x14DeleteSessionRequest2\x8b\x03\n" +
"\vAuthService\x12\x8b\x01\n" +
"\x11GetCurrentSession\x12&.memos.api.v1.GetCurrentSessionRequest\x1a'.memos.api.v1.GetCurrentSessionResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/auth/sessions/current\x12z\n" +
"\rCreateSession\x12\".memos.api.v1.CreateSessionRequest\x1a#.memos.api.v1.CreateSessionResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/sessions\x12r\n" +
"\rDeleteSession\x12\".memos.api.v1.DeleteSessionRequest\x1a\x16.google.protobuf.Empty\"%\x82\xd3\xe4\x93\x02\x1f*\x1d/api/v1/auth/sessions/currentB\xa8\x01\n" +
"\vcredentials\"\xae\x01\n" +
"\x0eSignInResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12!\n" +
"\faccess_token\x18\x02 \x01(\tR\vaccessToken\x12Q\n" +
"\x17access_token_expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x14accessTokenExpiresAt\"\x10\n" +
"\x0eSignOutRequest\"\x15\n" +
"\x13RefreshTokenRequest\"t\n" +
"\x14RefreshTokenResponse\x12!\n" +
"\faccess_token\x18\x01 \x01(\tR\vaccessToken\x129\n" +
"\n" +
"expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt2\xbf\x03\n" +
"\vAuthService\x12t\n" +
"\x0eGetCurrentUser\x12#.memos.api.v1.GetCurrentUserRequest\x1a$.memos.api.v1.GetCurrentUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/v1/auth/me\x12c\n" +
"\x06SignIn\x12\x1b.memos.api.v1.SignInRequest\x1a\x1c.memos.api.v1.SignInResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/api/v1/auth/signin\x12]\n" +
"\aSignOut\x12\x1c.memos.api.v1.SignOutRequest\x1a\x16.google.protobuf.Empty\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x14/api/v1/auth/signout\x12v\n" +
"\fRefreshToken\x12!.memos.api.v1.RefreshTokenRequest\x1a\".memos.api.v1.RefreshTokenResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/auth/refreshB\xa8\x01\n" +
"\x10com.memos.api.v1B\x10AuthServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3"
var (
@ -469,37 +560,41 @@ func file_api_v1_auth_service_proto_rawDescGZIP() []byte {
return file_api_v1_auth_service_proto_rawDescData
}
var file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_api_v1_auth_service_proto_goTypes = []any{
(*GetCurrentSessionRequest)(nil), // 0: memos.api.v1.GetCurrentSessionRequest
(*GetCurrentSessionResponse)(nil), // 1: memos.api.v1.GetCurrentSessionResponse
(*CreateSessionRequest)(nil), // 2: memos.api.v1.CreateSessionRequest
(*CreateSessionResponse)(nil), // 3: memos.api.v1.CreateSessionResponse
(*DeleteSessionRequest)(nil), // 4: memos.api.v1.DeleteSessionRequest
(*CreateSessionRequest_PasswordCredentials)(nil), // 5: memos.api.v1.CreateSessionRequest.PasswordCredentials
(*CreateSessionRequest_SSOCredentials)(nil), // 6: memos.api.v1.CreateSessionRequest.SSOCredentials
(*User)(nil), // 7: memos.api.v1.User
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
(*GetCurrentUserRequest)(nil), // 0: memos.api.v1.GetCurrentUserRequest
(*GetCurrentUserResponse)(nil), // 1: memos.api.v1.GetCurrentUserResponse
(*SignInRequest)(nil), // 2: memos.api.v1.SignInRequest
(*SignInResponse)(nil), // 3: memos.api.v1.SignInResponse
(*SignOutRequest)(nil), // 4: memos.api.v1.SignOutRequest
(*RefreshTokenRequest)(nil), // 5: memos.api.v1.RefreshTokenRequest
(*RefreshTokenResponse)(nil), // 6: memos.api.v1.RefreshTokenResponse
(*SignInRequest_PasswordCredentials)(nil), // 7: memos.api.v1.SignInRequest.PasswordCredentials
(*SignInRequest_SSOCredentials)(nil), // 8: memos.api.v1.SignInRequest.SSOCredentials
(*User)(nil), // 9: memos.api.v1.User
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 11: google.protobuf.Empty
}
var file_api_v1_auth_service_proto_depIdxs = []int32{
7, // 0: memos.api.v1.GetCurrentSessionResponse.user:type_name -> memos.api.v1.User
8, // 1: memos.api.v1.GetCurrentSessionResponse.last_accessed_at:type_name -> google.protobuf.Timestamp
5, // 2: memos.api.v1.CreateSessionRequest.password_credentials:type_name -> memos.api.v1.CreateSessionRequest.PasswordCredentials
6, // 3: memos.api.v1.CreateSessionRequest.sso_credentials:type_name -> memos.api.v1.CreateSessionRequest.SSOCredentials
7, // 4: memos.api.v1.CreateSessionResponse.user:type_name -> memos.api.v1.User
8, // 5: memos.api.v1.CreateSessionResponse.last_accessed_at:type_name -> google.protobuf.Timestamp
0, // 6: memos.api.v1.AuthService.GetCurrentSession:input_type -> memos.api.v1.GetCurrentSessionRequest
2, // 7: memos.api.v1.AuthService.CreateSession:input_type -> memos.api.v1.CreateSessionRequest
4, // 8: memos.api.v1.AuthService.DeleteSession:input_type -> memos.api.v1.DeleteSessionRequest
1, // 9: memos.api.v1.AuthService.GetCurrentSession:output_type -> memos.api.v1.GetCurrentSessionResponse
3, // 10: memos.api.v1.AuthService.CreateSession:output_type -> memos.api.v1.CreateSessionResponse
9, // 11: memos.api.v1.AuthService.DeleteSession:output_type -> google.protobuf.Empty
9, // [9:12] is the sub-list for method output_type
6, // [6:9] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
9, // 0: memos.api.v1.GetCurrentUserResponse.user:type_name -> memos.api.v1.User
7, // 1: memos.api.v1.SignInRequest.password_credentials:type_name -> memos.api.v1.SignInRequest.PasswordCredentials
8, // 2: memos.api.v1.SignInRequest.sso_credentials:type_name -> memos.api.v1.SignInRequest.SSOCredentials
9, // 3: memos.api.v1.SignInResponse.user:type_name -> memos.api.v1.User
10, // 4: memos.api.v1.SignInResponse.access_token_expires_at:type_name -> google.protobuf.Timestamp
10, // 5: memos.api.v1.RefreshTokenResponse.expires_at:type_name -> google.protobuf.Timestamp
0, // 6: memos.api.v1.AuthService.GetCurrentUser:input_type -> memos.api.v1.GetCurrentUserRequest
2, // 7: memos.api.v1.AuthService.SignIn:input_type -> memos.api.v1.SignInRequest
4, // 8: memos.api.v1.AuthService.SignOut:input_type -> memos.api.v1.SignOutRequest
5, // 9: memos.api.v1.AuthService.RefreshToken:input_type -> memos.api.v1.RefreshTokenRequest
1, // 10: memos.api.v1.AuthService.GetCurrentUser:output_type -> memos.api.v1.GetCurrentUserResponse
3, // 11: memos.api.v1.AuthService.SignIn:output_type -> memos.api.v1.SignInResponse
11, // 12: memos.api.v1.AuthService.SignOut:output_type -> google.protobuf.Empty
6, // 13: memos.api.v1.AuthService.RefreshToken:output_type -> memos.api.v1.RefreshTokenResponse
10, // [10:14] is the sub-list for method output_type
6, // [6:10] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_api_v1_auth_service_proto_init() }
@ -509,8 +604,8 @@ func file_api_v1_auth_service_proto_init() {
}
file_api_v1_user_service_proto_init()
file_api_v1_auth_service_proto_msgTypes[2].OneofWrappers = []any{
(*CreateSessionRequest_PasswordCredentials_)(nil),
(*CreateSessionRequest_SsoCredentials)(nil),
(*SignInRequest_PasswordCredentials_)(nil),
(*SignInRequest_SsoCredentials)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@ -518,7 +613,7 @@ func file_api_v1_auth_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -35,30 +35,30 @@ var (
_ = metadata.Join
)
func request_AuthService_GetCurrentSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetCurrentSessionRequest
protoReq GetCurrentUserRequest
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetCurrentSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.GetCurrentUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_AuthService_GetCurrentSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetCurrentSessionRequest
protoReq GetCurrentUserRequest
metadata runtime.ServerMetadata
)
msg, err := server.GetCurrentSession(ctx, &protoReq)
msg, err := server.GetCurrentUser(ctx, &protoReq)
return msg, metadata, err
}
func request_AuthService_CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateSessionRequest
protoReq SignInRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
@ -67,40 +67,67 @@ func request_AuthService_CreateSession_0(ctx context.Context, marshaler runtime.
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.CreateSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.SignIn(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_AuthService_CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateSessionRequest
protoReq SignInRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.CreateSession(ctx, &protoReq)
msg, err := server.SignIn(ctx, &protoReq)
return msg, metadata, err
}
func request_AuthService_DeleteSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteSessionRequest
protoReq SignOutRequest
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.DeleteSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.SignOut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_AuthService_DeleteSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteSessionRequest
protoReq SignOutRequest
metadata runtime.ServerMetadata
)
msg, err := server.DeleteSession(ctx, &protoReq)
msg, err := server.SignOut(ctx, &protoReq)
return msg, metadata, err
}
func request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq RefreshTokenRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.RefreshToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq RefreshTokenRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.RefreshToken(ctx, &protoReq)
return msg, metadata, err
}
@ -110,65 +137,85 @@ func local_request_AuthService_DeleteSession_0(ctx context.Context, marshaler ru
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error {
mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentUser", runtime.WithHTTPPathPattern("/api/v1/auth/me"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_AuthService_GetCurrentSession_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_AuthService_GetCurrentUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_GetCurrentSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_AuthService_CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_AuthService_SignIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/CreateSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/SignIn", runtime.WithHTTPPathPattern("/api/v1/auth/signin"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_AuthService_CreateSession_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_AuthService_SignIn_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_CreateSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_AuthService_DeleteSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_AuthService_SignOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/DeleteSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/SignOut", runtime.WithHTTPPathPattern("/api/v1/auth/signout"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_AuthService_DeleteSession_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_AuthService_SignOut_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_DeleteSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/RefreshToken", runtime.WithHTTPPathPattern("/api/v1/auth/refresh"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_AuthService_RefreshToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
@ -210,68 +257,87 @@ func RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "AuthServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error {
mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentUser", runtime.WithHTTPPathPattern("/api/v1/auth/me"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AuthService_GetCurrentSession_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_AuthService_GetCurrentUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_GetCurrentSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_AuthService_CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_AuthService_SignIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/CreateSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/SignIn", runtime.WithHTTPPathPattern("/api/v1/auth/signin"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AuthService_CreateSession_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_AuthService_SignIn_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_CreateSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_AuthService_DeleteSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_AuthService_SignOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/DeleteSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/SignOut", runtime.WithHTTPPathPattern("/api/v1/auth/signout"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AuthService_DeleteSession_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_AuthService_SignOut_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_DeleteSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_AuthService_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/RefreshToken", runtime.WithHTTPPathPattern("/api/v1/auth/refresh"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_AuthService_RefreshToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_AuthService_GetCurrentSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "auth", "sessions", "current"}, ""))
pattern_AuthService_CreateSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "sessions"}, ""))
pattern_AuthService_DeleteSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "auth", "sessions", "current"}, ""))
pattern_AuthService_GetCurrentUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "me"}, ""))
pattern_AuthService_SignIn_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "signin"}, ""))
pattern_AuthService_SignOut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "signout"}, ""))
pattern_AuthService_RefreshToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "refresh"}, ""))
)
var (
forward_AuthService_GetCurrentSession_0 = runtime.ForwardResponseMessage
forward_AuthService_CreateSession_0 = runtime.ForwardResponseMessage
forward_AuthService_DeleteSession_0 = runtime.ForwardResponseMessage
forward_AuthService_GetCurrentUser_0 = runtime.ForwardResponseMessage
forward_AuthService_SignIn_0 = runtime.ForwardResponseMessage
forward_AuthService_SignOut_0 = runtime.ForwardResponseMessage
forward_AuthService_RefreshToken_0 = runtime.ForwardResponseMessage
)

View File

@ -20,24 +20,31 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
AuthService_GetCurrentSession_FullMethodName = "/memos.api.v1.AuthService/GetCurrentSession"
AuthService_CreateSession_FullMethodName = "/memos.api.v1.AuthService/CreateSession"
AuthService_DeleteSession_FullMethodName = "/memos.api.v1.AuthService/DeleteSession"
AuthService_GetCurrentUser_FullMethodName = "/memos.api.v1.AuthService/GetCurrentUser"
AuthService_SignIn_FullMethodName = "/memos.api.v1.AuthService/SignIn"
AuthService_SignOut_FullMethodName = "/memos.api.v1.AuthService/SignOut"
AuthService_RefreshToken_FullMethodName = "/memos.api.v1.AuthService/RefreshToken"
)
// AuthServiceClient is the client API for AuthService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AuthServiceClient interface {
// GetCurrentSession returns the current active session information.
// This method is idempotent and safe, suitable for checking current session state.
GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error)
// CreateSession authenticates a user and creates a new session.
// Returns the authenticated user information upon successful authentication.
CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error)
// DeleteSession terminates the current user session.
// This is an idempotent operation that invalidates the user's authentication.
DeleteSession(ctx context.Context, in *DeleteSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
// Similar to OIDC's /userinfo endpoint.
GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error)
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
// Supports password-based and SSO authentication methods.
SignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error)
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// RefreshToken exchanges a valid refresh token for a new access token.
// The refresh token is read from the HttpOnly cookie.
// Returns a new short-lived access token.
RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error)
}
type authServiceClient struct {
@ -48,30 +55,40 @@ func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient {
return &authServiceClient{cc}
}
func (c *authServiceClient) GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error) {
func (c *authServiceClient) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetCurrentSessionResponse)
err := c.cc.Invoke(ctx, AuthService_GetCurrentSession_FullMethodName, in, out, cOpts...)
out := new(GetCurrentUserResponse)
err := c.cc.Invoke(ctx, AuthService_GetCurrentUser_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) {
func (c *authServiceClient) SignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateSessionResponse)
err := c.cc.Invoke(ctx, AuthService_CreateSession_FullMethodName, in, out, cOpts...)
out := new(SignInResponse)
err := c.cc.Invoke(ctx, AuthService_SignIn_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) DeleteSession(ctx context.Context, in *DeleteSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
func (c *authServiceClient) SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, AuthService_DeleteSession_FullMethodName, in, out, cOpts...)
err := c.cc.Invoke(ctx, AuthService_SignOut_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RefreshTokenResponse)
err := c.cc.Invoke(ctx, AuthService_RefreshToken_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@ -82,15 +99,21 @@ func (c *authServiceClient) DeleteSession(ctx context.Context, in *DeleteSession
// All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility.
type AuthServiceServer interface {
// GetCurrentSession returns the current active session information.
// This method is idempotent and safe, suitable for checking current session state.
GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error)
// CreateSession authenticates a user and creates a new session.
// Returns the authenticated user information upon successful authentication.
CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error)
// DeleteSession terminates the current user session.
// This is an idempotent operation that invalidates the user's authentication.
DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error)
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
// Similar to OIDC's /userinfo endpoint.
GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error)
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
// Supports password-based and SSO authentication methods.
SignIn(context.Context, *SignInRequest) (*SignInResponse, error)
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
SignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error)
// RefreshToken exchanges a valid refresh token for a new access token.
// The refresh token is read from the HttpOnly cookie.
// Returns a new short-lived access token.
RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error)
mustEmbedUnimplementedAuthServiceServer()
}
@ -101,14 +124,17 @@ type AuthServiceServer interface {
// pointer dereference when methods are called.
type UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCurrentSession not implemented")
func (UnimplementedAuthServiceServer) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCurrentUser not implemented")
}
func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateSession not implemented")
func (UnimplementedAuthServiceServer) SignIn(context.Context, *SignInRequest) (*SignInResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SignIn not implemented")
}
func (UnimplementedAuthServiceServer) DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteSession not implemented")
func (UnimplementedAuthServiceServer) SignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SignOut not implemented")
}
func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
@ -131,56 +157,74 @@ func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
s.RegisterService(&AuthService_ServiceDesc, srv)
}
func _AuthService_GetCurrentSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetCurrentSessionRequest)
func _AuthService_GetCurrentUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetCurrentUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).GetCurrentSession(ctx, in)
return srv.(AuthServiceServer).GetCurrentUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_GetCurrentSession_FullMethodName,
FullMethod: AuthService_GetCurrentUser_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).GetCurrentSession(ctx, req.(*GetCurrentSessionRequest))
return srv.(AuthServiceServer).GetCurrentUser(ctx, req.(*GetCurrentUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateSessionRequest)
func _AuthService_SignIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SignInRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).CreateSession(ctx, in)
return srv.(AuthServiceServer).SignIn(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_CreateSession_FullMethodName,
FullMethod: AuthService_SignIn_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).CreateSession(ctx, req.(*CreateSessionRequest))
return srv.(AuthServiceServer).SignIn(ctx, req.(*SignInRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_DeleteSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteSessionRequest)
func _AuthService_SignOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SignOutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).DeleteSession(ctx, in)
return srv.(AuthServiceServer).SignOut(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_DeleteSession_FullMethodName,
FullMethod: AuthService_SignOut_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).DeleteSession(ctx, req.(*DeleteSessionRequest))
return srv.(AuthServiceServer).SignOut(ctx, req.(*SignOutRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_RefreshToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RefreshTokenRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).RefreshToken(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_RefreshToken_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).RefreshToken(ctx, req.(*RefreshTokenRequest))
}
return interceptor(ctx, in, info, handler)
}
@ -193,16 +237,20 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
HandlerType: (*AuthServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetCurrentSession",
Handler: _AuthService_GetCurrentSession_Handler,
MethodName: "GetCurrentUser",
Handler: _AuthService_GetCurrentUser_Handler,
},
{
MethodName: "CreateSession",
Handler: _AuthService_CreateSession_Handler,
MethodName: "SignIn",
Handler: _AuthService_SignIn_Handler,
},
{
MethodName: "DeleteSession",
Handler: _AuthService_DeleteSession_Handler,
MethodName: "SignOut",
Handler: _AuthService_SignOut_Handler,
},
{
MethodName: "RefreshToken",
Handler: _AuthService_RefreshToken_Handler,
},
},
Streams: []grpc.StreamDesc{},

File diff suppressed because it is too large Load Diff

View File

@ -531,11 +531,11 @@ func local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler
return msg, metadata, err
}
var filter_UserService_ListUserAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
var filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
func request_UserService_ListUserAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserAccessTokensRequest
protoReq ListPersonalAccessTokensRequest
metadata runtime.ServerMetadata
err error
)
@ -553,16 +553,16 @@ func request_UserService_ListUserAccessTokens_0(ctx context.Context, marshaler r
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserAccessTokens_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListPersonalAccessTokens_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ListUserAccessTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.ListPersonalAccessTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_ListUserAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserAccessTokensRequest
protoReq ListPersonalAccessTokensRequest
metadata runtime.ServerMetadata
err error
)
@ -577,22 +577,20 @@ func local_request_UserService_ListUserAccessTokens_0(ctx context.Context, marsh
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserAccessTokens_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListPersonalAccessTokens_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ListUserAccessTokens(ctx, &protoReq)
msg, err := server.ListPersonalAccessTokens(ctx, &protoReq)
return msg, metadata, err
}
var filter_UserService_CreateUserAccessToken_0 = &utilities.DoubleArray{Encoding: map[string]int{"access_token": 0, "parent": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}
func request_UserService_CreateUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateUserAccessTokenRequest
protoReq CreatePersonalAccessTokenRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.AccessToken); err != nil && !errors.Is(err, io.EOF) {
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
@ -606,23 +604,17 @@ func request_UserService_CreateUserAccessToken_0(ctx context.Context, marshaler
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUserAccessToken_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.CreateUserAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.CreatePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_CreateUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateUserAccessTokenRequest
protoReq CreatePersonalAccessTokenRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.AccessToken); err != nil && !errors.Is(err, io.EOF) {
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["parent"]
@ -633,19 +625,13 @@ func local_request_UserService_CreateUserAccessToken_0(ctx context.Context, mars
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUserAccessToken_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.CreateUserAccessToken(ctx, &protoReq)
msg, err := server.CreatePersonalAccessToken(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_DeleteUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteUserAccessTokenRequest
protoReq DeletePersonalAccessTokenRequest
metadata runtime.ServerMetadata
err error
)
@ -660,13 +646,13 @@ func request_UserService_DeleteUserAccessToken_0(ctx context.Context, marshaler
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.DeleteUserAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.DeletePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteUserAccessTokenRequest
protoReq DeletePersonalAccessTokenRequest
metadata runtime.ServerMetadata
err error
)
@ -678,13 +664,13 @@ func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, mars
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.DeleteUserAccessToken(ctx, &protoReq)
msg, err := server.DeletePersonalAccessToken(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_ListUserSessions_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_UserService_ListSessions_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserSessionsRequest
protoReq ListSessionsRequest
metadata runtime.ServerMetadata
err error
)
@ -699,13 +685,13 @@ func request_UserService_ListUserSessions_0(ctx context.Context, marshaler runti
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := client.ListUserSessions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.ListSessions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_ListUserSessions_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_UserService_ListSessions_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListUserSessionsRequest
protoReq ListSessionsRequest
metadata runtime.ServerMetadata
err error
)
@ -717,13 +703,13 @@ func local_request_UserService_ListUserSessions_0(ctx context.Context, marshaler
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := server.ListUserSessions(ctx, &protoReq)
msg, err := server.ListSessions(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_RevokeUserSession_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func request_UserService_RevokeSession_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq RevokeUserSessionRequest
protoReq RevokeSessionRequest
metadata runtime.ServerMetadata
err error
)
@ -738,13 +724,13 @@ func request_UserService_RevokeUserSession_0(ctx context.Context, marshaler runt
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.RevokeUserSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.RevokeSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_RevokeUserSession_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
func local_request_UserService_RevokeSession_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq RevokeUserSessionRequest
protoReq RevokeSessionRequest
metadata runtime.ServerMetadata
err error
)
@ -756,7 +742,7 @@ func local_request_UserService_RevokeUserSession_0(ctx context.Context, marshale
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.RevokeUserSession(ctx, &protoReq)
msg, err := server.RevokeSession(ctx, &protoReq)
return msg, metadata, err
}
@ -1343,105 +1329,105 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListPersonalAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_ListUserAccessTokens_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_UserService_ListPersonalAccessTokens_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreatePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_CreateUserAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_UserService_CreatePersonalAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/accessTokens/*}"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeletePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/personalAccessTokens/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_DeleteUserAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_UserService_DeletePersonalAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_UserService_ListSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_ListUserSessions_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_UserService_ListSessions_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_ListSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_RevokeUserSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodDelete, pattern_UserService_RevokeSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeUserSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_RevokeUserSession_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_UserService_RevokeSession_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_RevokeUserSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_RevokeSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
@ -1793,90 +1779,90 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListPersonalAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_ListUserAccessTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_UserService_ListPersonalAccessTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreatePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/personalAccessTokens"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_CreateUserAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_UserService_CreatePersonalAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/accessTokens/*}"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeletePersonalAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/personalAccessTokens/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_DeleteUserAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_UserService_DeletePersonalAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_UserService_ListSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_ListUserSessions_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_UserService_ListSessions_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListUserSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_ListSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_RevokeUserSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodDelete, pattern_UserService_RevokeSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeUserSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_RevokeUserSession_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_UserService_RevokeSession_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_RevokeUserSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_UserService_RevokeSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
@ -2001,51 +1987,51 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
}
var (
pattern_UserService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats"))
pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats"))
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "setting.name"}, ""))
pattern_UserService_ListUserSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "settings"}, ""))
pattern_UserService_ListUserAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "accessTokens"}, ""))
pattern_UserService_CreateUserAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "accessTokens"}, ""))
pattern_UserService_DeleteUserAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "accessTokens", "name"}, ""))
pattern_UserService_ListUserSessions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "sessions"}, ""))
pattern_UserService_RevokeUserSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "sessions", "name"}, ""))
pattern_UserService_ListUserWebhooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, ""))
pattern_UserService_CreateUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, ""))
pattern_UserService_UpdateUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "webhook.name"}, ""))
pattern_UserService_DeleteUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, ""))
pattern_UserService_ListUserNotifications_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "notifications"}, ""))
pattern_UserService_UpdateUserNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "notifications", "notification.name"}, ""))
pattern_UserService_DeleteUserNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "notifications", "name"}, ""))
pattern_UserService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats"))
pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats"))
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "setting.name"}, ""))
pattern_UserService_ListUserSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "settings"}, ""))
pattern_UserService_ListPersonalAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_CreatePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_DeletePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "personalAccessTokens", "name"}, ""))
pattern_UserService_ListSessions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "sessions"}, ""))
pattern_UserService_RevokeSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "sessions", "name"}, ""))
pattern_UserService_ListUserWebhooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, ""))
pattern_UserService_CreateUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, ""))
pattern_UserService_UpdateUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "webhook.name"}, ""))
pattern_UserService_DeleteUserWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, ""))
pattern_UserService_ListUserNotifications_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "notifications"}, ""))
pattern_UserService_UpdateUserNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "notifications", "notification.name"}, ""))
pattern_UserService_DeleteUserNotification_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "notifications", "name"}, ""))
)
var (
forward_UserService_ListUsers_0 = runtime.ForwardResponseMessage
forward_UserService_GetUser_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserAccessTokens_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUserAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSessions_0 = runtime.ForwardResponseMessage
forward_UserService_RevokeUserSession_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserWebhooks_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserNotifications_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserNotification_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserNotification_0 = runtime.ForwardResponseMessage
forward_UserService_ListUsers_0 = runtime.ForwardResponseMessage
forward_UserService_GetUser_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage
forward_UserService_ListPersonalAccessTokens_0 = runtime.ForwardResponseMessage
forward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_ListSessions_0 = runtime.ForwardResponseMessage
forward_UserService_RevokeSession_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserWebhooks_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserWebhook_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserNotifications_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserNotification_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUserNotification_0 = runtime.ForwardResponseMessage
)

View File

@ -20,28 +20,28 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
UserService_ListUsers_FullMethodName = "/memos.api.v1.UserService/ListUsers"
UserService_GetUser_FullMethodName = "/memos.api.v1.UserService/GetUser"
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings"
UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens"
UserService_CreateUserAccessToken_FullMethodName = "/memos.api.v1.UserService/CreateUserAccessToken"
UserService_DeleteUserAccessToken_FullMethodName = "/memos.api.v1.UserService/DeleteUserAccessToken"
UserService_ListUserSessions_FullMethodName = "/memos.api.v1.UserService/ListUserSessions"
UserService_RevokeUserSession_FullMethodName = "/memos.api.v1.UserService/RevokeUserSession"
UserService_ListUserWebhooks_FullMethodName = "/memos.api.v1.UserService/ListUserWebhooks"
UserService_CreateUserWebhook_FullMethodName = "/memos.api.v1.UserService/CreateUserWebhook"
UserService_UpdateUserWebhook_FullMethodName = "/memos.api.v1.UserService/UpdateUserWebhook"
UserService_DeleteUserWebhook_FullMethodName = "/memos.api.v1.UserService/DeleteUserWebhook"
UserService_ListUserNotifications_FullMethodName = "/memos.api.v1.UserService/ListUserNotifications"
UserService_UpdateUserNotification_FullMethodName = "/memos.api.v1.UserService/UpdateUserNotification"
UserService_DeleteUserNotification_FullMethodName = "/memos.api.v1.UserService/DeleteUserNotification"
UserService_ListUsers_FullMethodName = "/memos.api.v1.UserService/ListUsers"
UserService_GetUser_FullMethodName = "/memos.api.v1.UserService/GetUser"
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings"
UserService_ListPersonalAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListPersonalAccessTokens"
UserService_CreatePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/CreatePersonalAccessToken"
UserService_DeletePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/DeletePersonalAccessToken"
UserService_ListSessions_FullMethodName = "/memos.api.v1.UserService/ListSessions"
UserService_RevokeSession_FullMethodName = "/memos.api.v1.UserService/RevokeSession"
UserService_ListUserWebhooks_FullMethodName = "/memos.api.v1.UserService/ListUserWebhooks"
UserService_CreateUserWebhook_FullMethodName = "/memos.api.v1.UserService/CreateUserWebhook"
UserService_UpdateUserWebhook_FullMethodName = "/memos.api.v1.UserService/UpdateUserWebhook"
UserService_DeleteUserWebhook_FullMethodName = "/memos.api.v1.UserService/DeleteUserWebhook"
UserService_ListUserNotifications_FullMethodName = "/memos.api.v1.UserService/ListUserNotifications"
UserService_UpdateUserNotification_FullMethodName = "/memos.api.v1.UserService/UpdateUserNotification"
UserService_DeleteUserNotification_FullMethodName = "/memos.api.v1.UserService/DeleteUserNotification"
)
// UserServiceClient is the client API for UserService service.
@ -71,16 +71,21 @@ type UserServiceClient interface {
UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)
// ListUserSettings returns a list of user settings.
ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error)
// ListUserAccessTokens returns a list of access tokens for a user.
ListUserAccessTokens(ctx context.Context, in *ListUserAccessTokensRequest, opts ...grpc.CallOption) (*ListUserAccessTokensResponse, error)
// CreateUserAccessToken creates a new access token for a user.
CreateUserAccessToken(ctx context.Context, in *CreateUserAccessTokenRequest, opts ...grpc.CallOption) (*UserAccessToken, error)
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken(ctx context.Context, in *DeleteUserAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions(ctx context.Context, in *ListUserSessionsRequest, opts ...grpc.CallOption) (*ListUserSessionsResponse, error)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession(ctx context.Context, in *RevokeUserSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error)
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
// The token value is only returned once upon creation.
CreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error)
// DeletePersonalAccessToken deletes a Personal Access Token.
DeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListSessions returns a list of active login sessions for a user.
// Each session represents a browser/device where the user is logged in.
// Sessions are backed by refresh tokens with sliding expiration.
ListSessions(ctx context.Context, in *ListSessionsRequest, opts ...grpc.CallOption) (*ListSessionsResponse, error)
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
RevokeSession(ctx context.Context, in *RevokeSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListUserWebhooks returns a list of webhooks for a user.
ListUserWebhooks(ctx context.Context, in *ListUserWebhooksRequest, opts ...grpc.CallOption) (*ListUserWebhooksResponse, error)
// CreateUserWebhook creates a new webhook for a user.
@ -205,50 +210,50 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, in *ListUserSe
return out, nil
}
func (c *userServiceClient) ListUserAccessTokens(ctx context.Context, in *ListUserAccessTokensRequest, opts ...grpc.CallOption) (*ListUserAccessTokensResponse, error) {
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserAccessTokensResponse)
err := c.cc.Invoke(ctx, UserService_ListUserAccessTokens_FullMethodName, in, out, cOpts...)
out := new(ListPersonalAccessTokensResponse)
err := c.cc.Invoke(ctx, UserService_ListPersonalAccessTokens_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) CreateUserAccessToken(ctx context.Context, in *CreateUserAccessTokenRequest, opts ...grpc.CallOption) (*UserAccessToken, error) {
func (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserAccessToken)
err := c.cc.Invoke(ctx, UserService_CreateUserAccessToken_FullMethodName, in, out, cOpts...)
out := new(CreatePersonalAccessTokenResponse)
err := c.cc.Invoke(ctx, UserService_CreatePersonalAccessToken_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, in *DeleteUserAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
func (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, UserService_DeleteUserAccessToken_FullMethodName, in, out, cOpts...)
err := c.cc.Invoke(ctx, UserService_DeletePersonalAccessToken_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) ListUserSessions(ctx context.Context, in *ListUserSessionsRequest, opts ...grpc.CallOption) (*ListUserSessionsResponse, error) {
func (c *userServiceClient) ListSessions(ctx context.Context, in *ListSessionsRequest, opts ...grpc.CallOption) (*ListSessionsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserSessionsResponse)
err := c.cc.Invoke(ctx, UserService_ListUserSessions_FullMethodName, in, out, cOpts...)
out := new(ListSessionsResponse)
err := c.cc.Invoke(ctx, UserService_ListSessions_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) RevokeUserSession(ctx context.Context, in *RevokeUserSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
func (c *userServiceClient) RevokeSession(ctx context.Context, in *RevokeSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, UserService_RevokeUserSession_FullMethodName, in, out, cOpts...)
err := c.cc.Invoke(ctx, UserService_RevokeSession_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@ -352,16 +357,21 @@ type UserServiceServer interface {
UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error)
// ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error)
// ListUserAccessTokens returns a list of access tokens for a user.
ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error)
// CreateUserAccessToken creates a new access token for a user.
CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error)
// DeleteUserAccessToken deletes an access token.
DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error)
// ListUserSessions returns a list of active sessions for a user.
ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error)
// RevokeUserSession revokes a specific session for a user.
RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error)
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
// The token value is only returned once upon creation.
CreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error)
// DeletePersonalAccessToken deletes a Personal Access Token.
DeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error)
// ListSessions returns a list of active login sessions for a user.
// Each session represents a browser/device where the user is logged in.
// Sessions are backed by refresh tokens with sliding expiration.
ListSessions(context.Context, *ListSessionsRequest) (*ListSessionsResponse, error)
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
RevokeSession(context.Context, *RevokeSessionRequest) (*emptypb.Empty, error)
// ListUserWebhooks returns a list of webhooks for a user.
ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error)
// CreateUserWebhook creates a new webhook for a user.
@ -416,20 +426,20 @@ func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *Update
func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented")
}
func (UnimplementedUserServiceServer) ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserAccessTokens not implemented")
func (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPersonalAccessTokens not implemented")
}
func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error) {
return nil, status.Error(codes.Unimplemented, "method CreateUserAccessToken not implemented")
func (UnimplementedUserServiceServer) CreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreatePersonalAccessToken not implemented")
}
func (UnimplementedUserServiceServer) DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteUserAccessToken not implemented")
func (UnimplementedUserServiceServer) DeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeletePersonalAccessToken not implemented")
}
func (UnimplementedUserServiceServer) ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserSessions not implemented")
func (UnimplementedUserServiceServer) ListSessions(context.Context, *ListSessionsRequest) (*ListSessionsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListSessions not implemented")
}
func (UnimplementedUserServiceServer) RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method RevokeUserSession not implemented")
func (UnimplementedUserServiceServer) RevokeSession(context.Context, *RevokeSessionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method RevokeSession not implemented")
}
func (UnimplementedUserServiceServer) ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserWebhooks not implemented")
@ -653,92 +663,92 @@ func _UserService_ListUserSettings_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _UserService_ListUserAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserAccessTokensRequest)
func _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPersonalAccessTokensRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).ListUserAccessTokens(ctx, in)
return srv.(UserServiceServer).ListPersonalAccessTokens(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_ListUserAccessTokens_FullMethodName,
FullMethod: UserService_ListPersonalAccessTokens_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListUserAccessTokens(ctx, req.(*ListUserAccessTokensRequest))
return srv.(UserServiceServer).ListPersonalAccessTokens(ctx, req.(*ListPersonalAccessTokensRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_CreateUserAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUserAccessTokenRequest)
func _UserService_CreatePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePersonalAccessTokenRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).CreateUserAccessToken(ctx, in)
return srv.(UserServiceServer).CreatePersonalAccessToken(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_CreateUserAccessToken_FullMethodName,
FullMethod: UserService_CreatePersonalAccessToken_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).CreateUserAccessToken(ctx, req.(*CreateUserAccessTokenRequest))
return srv.(UserServiceServer).CreatePersonalAccessToken(ctx, req.(*CreatePersonalAccessTokenRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_DeleteUserAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteUserAccessTokenRequest)
func _UserService_DeletePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeletePersonalAccessTokenRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).DeleteUserAccessToken(ctx, in)
return srv.(UserServiceServer).DeletePersonalAccessToken(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_DeleteUserAccessToken_FullMethodName,
FullMethod: UserService_DeletePersonalAccessToken_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).DeleteUserAccessToken(ctx, req.(*DeleteUserAccessTokenRequest))
return srv.(UserServiceServer).DeletePersonalAccessToken(ctx, req.(*DeletePersonalAccessTokenRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_ListUserSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserSessionsRequest)
func _UserService_ListSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSessionsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).ListUserSessions(ctx, in)
return srv.(UserServiceServer).ListSessions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_ListUserSessions_FullMethodName,
FullMethod: UserService_ListSessions_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListUserSessions(ctx, req.(*ListUserSessionsRequest))
return srv.(UserServiceServer).ListSessions(ctx, req.(*ListSessionsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_RevokeUserSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeUserSessionRequest)
func _UserService_RevokeSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeSessionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).RevokeUserSession(ctx, in)
return srv.(UserServiceServer).RevokeSession(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_RevokeUserSession_FullMethodName,
FullMethod: UserService_RevokeSession_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).RevokeUserSession(ctx, req.(*RevokeUserSessionRequest))
return srv.(UserServiceServer).RevokeSession(ctx, req.(*RevokeSessionRequest))
}
return interceptor(ctx, in, info, handler)
}
@ -917,24 +927,24 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
Handler: _UserService_ListUserSettings_Handler,
},
{
MethodName: "ListUserAccessTokens",
Handler: _UserService_ListUserAccessTokens_Handler,
MethodName: "ListPersonalAccessTokens",
Handler: _UserService_ListPersonalAccessTokens_Handler,
},
{
MethodName: "CreateUserAccessToken",
Handler: _UserService_CreateUserAccessToken_Handler,
MethodName: "CreatePersonalAccessToken",
Handler: _UserService_CreatePersonalAccessToken_Handler,
},
{
MethodName: "DeleteUserAccessToken",
Handler: _UserService_DeleteUserAccessToken_Handler,
MethodName: "DeletePersonalAccessToken",
Handler: _UserService_DeletePersonalAccessToken_Handler,
},
{
MethodName: "ListUserSessions",
Handler: _UserService_ListUserSessions_Handler,
MethodName: "ListSessions",
Handler: _UserService_ListSessions_Handler,
},
{
MethodName: "RevokeUserSession",
Handler: _UserService_RevokeUserSession_Handler,
MethodName: "RevokeSession",
Handler: _UserService_RevokeSession_Handler,
},
{
MethodName: "ListUserWebhooks",

View File

@ -239,19 +239,42 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/auth/sessions:
/api/v1/auth/me:
get:
tags:
- AuthService
description: |-
GetCurrentUser returns the authenticated user's information.
Validates the access token and returns user details.
Similar to OIDC's /userinfo endpoint.
operationId: AuthService_GetCurrentUser
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GetCurrentUserResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/auth/refresh:
post:
tags:
- AuthService
description: |-
CreateSession authenticates a user and creates a new session.
Returns the authenticated user information upon successful authentication.
operationId: AuthService_CreateSession
RefreshToken exchanges a valid refresh token for a new access token.
The refresh token is read from the HttpOnly cookie.
Returns a new short-lived access token.
operationId: AuthService_RefreshToken
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSessionRequest'
$ref: '#/components/schemas/RefreshTokenRequest'
required: true
responses:
"200":
@ -259,41 +282,49 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSessionResponse'
$ref: '#/components/schemas/RefreshTokenResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/auth/sessions/current:
get:
/api/v1/auth/signin:
post:
tags:
- AuthService
description: |-
GetCurrentSession returns the current active session information.
This method is idempotent and safe, suitable for checking current session state.
operationId: AuthService_GetCurrentSession
SignIn authenticates a user with credentials and returns tokens.
On success, returns an access token and sets a refresh token cookie.
Supports password-based and SSO authentication methods.
operationId: AuthService_SignIn
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SignInRequest'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GetCurrentSessionResponse'
$ref: '#/components/schemas/SignInResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
delete:
/api/v1/auth/signout:
post:
tags:
- AuthService
description: |-
DeleteSession terminates the current user session.
This is an idempotent operation that invalidates the user's authentication.
operationId: AuthService_DeleteSession
SignOut terminates the user's authentication.
Revokes the refresh token and clears the authentication cookie.
operationId: AuthService_SignOut
responses:
"200":
description: OK
@ -1220,108 +1251,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/accessTokens:
get:
tags:
- UserService
description: ListUserAccessTokens returns a list of access tokens for a user.
operationId: UserService_ListUserAccessTokens
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: pageSize
in: query
description: Optional. The maximum number of access tokens to return.
schema:
type: integer
format: int32
- name: pageToken
in: query
description: Optional. A page token for pagination.
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ListUserAccessTokensResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
post:
tags:
- UserService
description: CreateUserAccessToken creates a new access token for a user.
operationId: UserService_CreateUserAccessToken
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: accessTokenId
in: query
description: Optional. The access token ID to use.
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserAccessToken'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/UserAccessToken'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/accessTokens/{accessToken}:
delete:
tags:
- UserService
description: DeleteUserAccessToken deletes an access token.
operationId: UserService_DeleteUserAccessToken
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: accessToken
in: path
description: The accessToken id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/notifications:
get:
tags:
@ -1432,12 +1361,116 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/personalAccessTokens:
get:
tags:
- UserService
description: |-
ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
operationId: UserService_ListPersonalAccessTokens
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: pageSize
in: query
description: Optional. The maximum number of tokens to return.
schema:
type: integer
format: int32
- name: pageToken
in: query
description: Optional. A page token for pagination.
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ListPersonalAccessTokensResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
post:
tags:
- UserService
description: |-
CreatePersonalAccessToken creates a new Personal Access Token for a user.
The token value is only returned once upon creation.
operationId: UserService_CreatePersonalAccessToken
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePersonalAccessTokenRequest'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePersonalAccessTokenResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/personalAccessTokens/{personalAccessToken}:
delete:
tags:
- UserService
description: DeletePersonalAccessToken deletes a Personal Access Token.
operationId: UserService_DeletePersonalAccessToken
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: personalAccessToken
in: path
description: The personalAccessToken id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/sessions:
get:
tags:
- UserService
description: ListUserSessions returns a list of active sessions for a user.
operationId: UserService_ListUserSessions
description: |-
ListSessions returns a list of active login sessions for a user.
Each session represents a browser/device where the user is logged in.
Sessions are backed by refresh tokens with sliding expiration.
operationId: UserService_ListSessions
parameters:
- name: user
in: path
@ -1451,7 +1484,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ListUserSessionsResponse'
$ref: '#/components/schemas/ListSessionsResponse'
default:
description: Default error response
content:
@ -1462,8 +1495,10 @@ paths:
delete:
tags:
- UserService
description: RevokeUserSession revokes a specific session for a user.
operationId: UserService_RevokeUserSession
description: |-
RevokeSession revokes a specific login session.
This invalidates the refresh token, forcing re-authentication on that device.
operationId: UserService_RevokeSession
parameters:
- name: user
in: path
@ -2049,76 +2084,35 @@ components:
description: |-
Optional. The related memo. Refer to `Memo.name`.
Format: memos/{memo}
CreateSessionRequest:
type: object
properties:
passwordCredentials:
allOf:
- $ref: '#/components/schemas/CreateSessionRequest_PasswordCredentials'
description: Username and password authentication method.
ssoCredentials:
allOf:
- $ref: '#/components/schemas/CreateSessionRequest_SSOCredentials'
description: SSO provider authentication method.
CreateSessionRequest_PasswordCredentials:
CreatePersonalAccessTokenRequest:
required:
- username
- password
- parent
type: object
properties:
username:
parent:
type: string
description: |-
The username to sign in with.
Required field for password-based authentication.
password:
Required. The parent resource where this token will be created.
Format: users/{user}
description:
type: string
description: |-
The password to sign in with.
Required field for password-based authentication.
description: Nested message for password-based authentication credentials.
CreateSessionRequest_SSOCredentials:
required:
- idpId
- code
- redirectUri
type: object
properties:
idpId:
description: Optional. Description of the personal access token.
expiresInDays:
type: integer
description: |-
The ID of the SSO provider.
Required field to identify the SSO provider.
description: Optional. Expiration duration in days (0 = never expires).
format: int32
code:
type: string
description: |-
The authorization code from the SSO provider.
Required field for completing the SSO flow.
redirectUri:
type: string
description: |-
The redirect URI used in the SSO flow.
Required field for security validation.
codeVerifier:
type: string
description: |-
The PKCE code verifier for enhanced security (RFC 7636).
Optional field - if provided, enables PKCE flow protection against authorization code interception.
description: Nested message for SSO authentication credentials.
CreateSessionResponse:
CreatePersonalAccessTokenResponse:
type: object
properties:
user:
personalAccessToken:
allOf:
- $ref: '#/components/schemas/User'
description: The authenticated user information.
lastAccessedAt:
- $ref: '#/components/schemas/PersonalAccessToken'
description: The personal access token metadata.
token:
type: string
description: |-
Last time the session was accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
format: date-time
The actual token value - only returned on creation.
This is the only time the token value will be visible.
FieldMapping:
type: object
properties:
@ -2140,17 +2134,13 @@ components:
logoUrl:
type: string
description: Custom profile configuration for instance branding.
GetCurrentSessionResponse:
GetCurrentUserResponse:
type: object
properties:
user:
$ref: '#/components/schemas/User'
lastAccessedAt:
type: string
description: |-
Last time the session was accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
format: date-time
allOf:
- $ref: '#/components/schemas/User'
description: The authenticated user's information.
GoogleProtobufAny:
type: object
properties:
@ -2426,6 +2416,29 @@ components:
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
ListPersonalAccessTokensResponse:
type: object
properties:
personalAccessTokens:
type: array
items:
$ref: '#/components/schemas/PersonalAccessToken'
description: The list of personal access tokens.
nextPageToken:
type: string
description: A token for the next page of results.
totalSize:
type: integer
description: The total count of personal access tokens.
format: int32
ListSessionsResponse:
type: object
properties:
sessions:
type: array
items:
$ref: '#/components/schemas/Session'
description: The list of sessions.
ListShortcutsResponse:
type: object
properties:
@ -2434,21 +2447,6 @@ components:
items:
$ref: '#/components/schemas/Shortcut'
description: The list of shortcuts.
ListUserAccessTokensResponse:
type: object
properties:
accessTokens:
type: array
items:
$ref: '#/components/schemas/UserAccessToken'
description: The list of access tokens.
nextPageToken:
type: string
description: A token for the next page of results.
totalSize:
type: integer
description: The total count of access tokens.
format: int32
ListUserNotificationsResponse:
type: object
properties:
@ -2458,14 +2456,6 @@ components:
$ref: '#/components/schemas/UserNotification'
nextPageToken:
type: string
ListUserSessionsResponse:
type: object
properties:
sessions:
type: array
items:
$ref: '#/components/schemas/UserSession'
description: The list of user sessions.
ListUserSettingsResponse:
type: object
properties:
@ -2687,6 +2677,34 @@ components:
type: string
fieldMapping:
$ref: '#/components/schemas/FieldMapping'
PersonalAccessToken:
type: object
properties:
name:
type: string
description: |-
The resource name of the personal access token.
Format: users/{user}/personalAccessTokens/{personal_access_token}
description:
type: string
description: The description of the token.
createdAt:
readOnly: true
type: string
description: Output only. The creation timestamp.
format: date-time
expiresAt:
type: string
description: Optional. The expiration timestamp.
format: date-time
lastUsedAt:
readOnly: true
type: string
description: Output only. The last used timestamp.
format: date-time
description: |-
PersonalAccessToken represents a long-lived token for API/script access.
PATs are distinct from short-lived JWT access tokens used for session authentication.
Reaction:
required:
- contentId
@ -2719,6 +2737,69 @@ components:
type: string
description: Output only. The creation timestamp.
format: date-time
RefreshTokenRequest:
type: object
properties: {}
RefreshTokenResponse:
type: object
properties:
accessToken:
type: string
description: The new short-lived access token.
expiresAt:
type: string
description: When the access token expires.
format: date-time
Session:
type: object
properties:
name:
type: string
description: |-
The resource name of the session.
Format: users/{user}/sessions/{session}
sessionId:
readOnly: true
type: string
description: The session ID.
createTime:
readOnly: true
type: string
description: The timestamp when the session was created.
format: date-time
lastAccessedTime:
readOnly: true
type: string
description: |-
The timestamp when the session was last accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
format: date-time
clientInfo:
readOnly: true
allOf:
- $ref: '#/components/schemas/Session_ClientInfo'
description: Client information associated with this session.
description: |-
Session represents a user's login session on a specific device/browser.
Sessions are backed by refresh tokens with sliding expiration.
Session_ClientInfo:
type: object
properties:
userAgent:
type: string
description: User agent string of the client.
ipAddress:
type: string
description: IP address of the client.
deviceType:
type: string
description: Optional. Device type (e.g., "mobile", "desktop", "tablet").
os:
type: string
description: Optional. Operating system (e.g., "iOS 17.0", "Windows 11").
browser:
type: string
description: Optional. Browser name and version (e.g., "Chrome 119.0").
SetMemoAttachmentsRequest:
required:
- name
@ -2767,6 +2848,71 @@ components:
filter:
type: string
description: The filter expression for the shortcut.
SignInRequest:
type: object
properties:
passwordCredentials:
allOf:
- $ref: '#/components/schemas/SignInRequest_PasswordCredentials'
description: Username and password authentication.
ssoCredentials:
allOf:
- $ref: '#/components/schemas/SignInRequest_SSOCredentials'
description: SSO provider authentication.
SignInRequest_PasswordCredentials:
required:
- username
- password
type: object
properties:
username:
type: string
description: The username to sign in with.
password:
type: string
description: The password to sign in with.
description: Nested message for password-based authentication credentials.
SignInRequest_SSOCredentials:
required:
- idpId
- code
- redirectUri
type: object
properties:
idpId:
type: integer
description: The ID of the SSO provider.
format: int32
code:
type: string
description: The authorization code from the SSO provider.
redirectUri:
type: string
description: The redirect URI used in the SSO flow.
codeVerifier:
type: string
description: |-
The PKCE code verifier for enhanced security (RFC 7636).
Optional - enables PKCE flow protection against authorization code interception.
description: Nested message for SSO authentication credentials.
SignInResponse:
type: object
properties:
user:
allOf:
- $ref: '#/components/schemas/User'
description: The authenticated user's information.
accessToken:
type: string
description: |-
The short-lived access token for API requests.
Store in memory only, not in localStorage.
accessTokenExpiresAt:
type: string
description: |-
When the access token expires.
Client should call RefreshToken before this time.
format: date-time
Status:
type: object
properties:
@ -2874,31 +3020,6 @@ components:
type: string
description: Output only. The last update timestamp.
format: date-time
UserAccessToken:
type: object
properties:
name:
type: string
description: |-
The resource name of the access token.
Format: users/{user}/accessTokens/{access_token}
accessToken:
readOnly: true
type: string
description: Output only. The access token value.
description:
type: string
description: The description of the access token.
issuedAt:
readOnly: true
type: string
description: Output only. The issued timestamp.
format: date-time
expiresAt:
type: string
description: Optional. The expiration timestamp.
format: date-time
description: User access token message
UserNotification:
type: object
properties:
@ -2939,53 +3060,6 @@ components:
type: integer
description: The activity ID associated with this notification.
format: int32
UserSession:
type: object
properties:
name:
type: string
description: |-
The resource name of the session.
Format: users/{user}/sessions/{session}
sessionId:
readOnly: true
type: string
description: The session ID.
createTime:
readOnly: true
type: string
description: The timestamp when the session was created.
format: date-time
lastAccessedTime:
readOnly: true
type: string
description: |-
The timestamp when the session was last accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
format: date-time
clientInfo:
readOnly: true
allOf:
- $ref: '#/components/schemas/UserSession_ClientInfo'
description: Client information associated with this session.
UserSession_ClientInfo:
type: object
properties:
userAgent:
type: string
description: User agent string of the client.
ipAddress:
type: string
description: IP address of the client.
deviceType:
type: string
description: Optional. Device type (e.g., "mobile", "desktop", "tablet").
os:
type: string
description: Optional. Operating system (e.g., "iOS 17.0", "Windows 11").
browser:
type: string
description: Optional. Browser name and version (e.g., "Chrome 119.0").
UserSetting:
type: object
properties:
@ -3007,12 +3081,12 @@ components:
UserSetting_AccessTokensSetting:
type: object
properties:
accessTokens:
personalAccessTokens:
type: array
items:
$ref: '#/components/schemas/UserAccessToken'
description: List of user access tokens.
description: User access tokens configuration.
$ref: '#/components/schemas/PersonalAccessToken'
description: List of personal access tokens (PATs).
description: Personal access tokens configuration.
UserSetting_GeneralSetting:
type: object
properties:
@ -3035,8 +3109,8 @@ components:
sessions:
type: array
items:
$ref: '#/components/schemas/UserSession'
description: List of active user sessions.
$ref: '#/components/schemas/Session'
description: List of active login sessions.
description: User authentication sessions configuration.
UserSetting_WebhooksSetting:
type: object

View File

@ -36,6 +36,10 @@ const (
UserSetting_SHORTCUTS UserSetting_Key = 4
// The webhooks of the user.
UserSetting_WEBHOOKS UserSetting_Key = 5
// Refresh tokens for the user.
UserSetting_REFRESH_TOKENS UserSetting_Key = 6
// Personal access tokens for the user.
UserSetting_PERSONAL_ACCESS_TOKENS UserSetting_Key = 7
)
// Enum value maps for UserSetting_Key.
@ -47,14 +51,18 @@ var (
3: "ACCESS_TOKENS",
4: "SHORTCUTS",
5: "WEBHOOKS",
6: "REFRESH_TOKENS",
7: "PERSONAL_ACCESS_TOKENS",
}
UserSetting_Key_value = map[string]int32{
"KEY_UNSPECIFIED": 0,
"GENERAL": 1,
"SESSIONS": 2,
"ACCESS_TOKENS": 3,
"SHORTCUTS": 4,
"WEBHOOKS": 5,
"KEY_UNSPECIFIED": 0,
"GENERAL": 1,
"SESSIONS": 2,
"ACCESS_TOKENS": 3,
"SHORTCUTS": 4,
"WEBHOOKS": 5,
"REFRESH_TOKENS": 6,
"PERSONAL_ACCESS_TOKENS": 7,
}
)
@ -96,6 +104,8 @@ type UserSetting struct {
// *UserSetting_AccessTokens
// *UserSetting_Shortcuts
// *UserSetting_Webhooks
// *UserSetting_RefreshTokens
// *UserSetting_PersonalAccessTokens
Value isUserSetting_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@ -197,6 +207,24 @@ func (x *UserSetting) GetWebhooks() *WebhooksUserSetting {
return nil
}
func (x *UserSetting) GetRefreshTokens() *RefreshTokensUserSetting {
if x != nil {
if x, ok := x.Value.(*UserSetting_RefreshTokens); ok {
return x.RefreshTokens
}
}
return nil
}
func (x *UserSetting) GetPersonalAccessTokens() *PersonalAccessTokensUserSetting {
if x != nil {
if x, ok := x.Value.(*UserSetting_PersonalAccessTokens); ok {
return x.PersonalAccessTokens
}
}
return nil
}
type isUserSetting_Value interface {
isUserSetting_Value()
}
@ -221,6 +249,14 @@ type UserSetting_Webhooks struct {
Webhooks *WebhooksUserSetting `protobuf:"bytes,7,opt,name=webhooks,proto3,oneof"`
}
type UserSetting_RefreshTokens struct {
RefreshTokens *RefreshTokensUserSetting `protobuf:"bytes,8,opt,name=refresh_tokens,json=refreshTokens,proto3,oneof"`
}
type UserSetting_PersonalAccessTokens struct {
PersonalAccessTokens *PersonalAccessTokensUserSetting `protobuf:"bytes,9,opt,name=personal_access_tokens,json=personalAccessTokens,proto3,oneof"`
}
func (*UserSetting_General) isUserSetting_Value() {}
func (*UserSetting_Sessions) isUserSetting_Value() {}
@ -231,6 +267,10 @@ func (*UserSetting_Shortcuts) isUserSetting_Value() {}
func (*UserSetting_Webhooks) isUserSetting_Value() {}
func (*UserSetting_RefreshTokens) isUserSetting_Value() {}
func (*UserSetting_PersonalAccessTokens) isUserSetting_Value() {}
type GeneralUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The user's locale.
@ -383,6 +423,94 @@ func (x *AccessTokensUserSetting) GetAccessTokens() []*AccessTokensUserSetting_A
return nil
}
type RefreshTokensUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
RefreshTokens []*RefreshTokensUserSetting_RefreshToken `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokensUserSetting) Reset() {
*x = RefreshTokensUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RefreshTokensUserSetting) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokensUserSetting) ProtoMessage() {}
func (x *RefreshTokensUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RefreshTokensUserSetting.ProtoReflect.Descriptor instead.
func (*RefreshTokensUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4}
}
func (x *RefreshTokensUserSetting) GetRefreshTokens() []*RefreshTokensUserSetting_RefreshToken {
if x != nil {
return x.RefreshTokens
}
return nil
}
type PersonalAccessTokensUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
Tokens []*PersonalAccessTokensUserSetting_PersonalAccessToken `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PersonalAccessTokensUserSetting) Reset() {
*x = PersonalAccessTokensUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PersonalAccessTokensUserSetting) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PersonalAccessTokensUserSetting) ProtoMessage() {}
func (x *PersonalAccessTokensUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PersonalAccessTokensUserSetting.ProtoReflect.Descriptor instead.
func (*PersonalAccessTokensUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5}
}
func (x *PersonalAccessTokensUserSetting) GetTokens() []*PersonalAccessTokensUserSetting_PersonalAccessToken {
if x != nil {
return x.Tokens
}
return nil
}
type ShortcutsUserSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
Shortcuts []*ShortcutsUserSetting_Shortcut `protobuf:"bytes,1,rep,name=shortcuts,proto3" json:"shortcuts,omitempty"`
@ -392,7 +520,7 @@ type ShortcutsUserSetting struct {
func (x *ShortcutsUserSetting) Reset() {
*x = ShortcutsUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[4]
mi := &file_store_user_setting_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -404,7 +532,7 @@ func (x *ShortcutsUserSetting) String() string {
func (*ShortcutsUserSetting) ProtoMessage() {}
func (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[4]
mi := &file_store_user_setting_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -417,7 +545,7 @@ func (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use ShortcutsUserSetting.ProtoReflect.Descriptor instead.
func (*ShortcutsUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4}
return file_store_user_setting_proto_rawDescGZIP(), []int{6}
}
func (x *ShortcutsUserSetting) GetShortcuts() []*ShortcutsUserSetting_Shortcut {
@ -436,7 +564,7 @@ type WebhooksUserSetting struct {
func (x *WebhooksUserSetting) Reset() {
*x = WebhooksUserSetting{}
mi := &file_store_user_setting_proto_msgTypes[5]
mi := &file_store_user_setting_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -448,7 +576,7 @@ func (x *WebhooksUserSetting) String() string {
func (*WebhooksUserSetting) ProtoMessage() {}
func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[5]
mi := &file_store_user_setting_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -461,7 +589,7 @@ func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebhooksUserSetting.ProtoReflect.Descriptor instead.
func (*WebhooksUserSetting) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5}
return file_store_user_setting_proto_rawDescGZIP(), []int{7}
}
func (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook {
@ -488,7 +616,7 @@ type SessionsUserSetting_Session struct {
func (x *SessionsUserSetting_Session) Reset() {
*x = SessionsUserSetting_Session{}
mi := &file_store_user_setting_proto_msgTypes[6]
mi := &file_store_user_setting_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -500,7 +628,7 @@ func (x *SessionsUserSetting_Session) String() string {
func (*SessionsUserSetting_Session) ProtoMessage() {}
func (x *SessionsUserSetting_Session) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[6]
mi := &file_store_user_setting_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -562,7 +690,7 @@ type SessionsUserSetting_ClientInfo struct {
func (x *SessionsUserSetting_ClientInfo) Reset() {
*x = SessionsUserSetting_ClientInfo{}
mi := &file_store_user_setting_proto_msgTypes[7]
mi := &file_store_user_setting_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -574,7 +702,7 @@ func (x *SessionsUserSetting_ClientInfo) String() string {
func (*SessionsUserSetting_ClientInfo) ProtoMessage() {}
func (x *SessionsUserSetting_ClientInfo) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[7]
mi := &file_store_user_setting_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -638,7 +766,7 @@ type AccessTokensUserSetting_AccessToken struct {
func (x *AccessTokensUserSetting_AccessToken) Reset() {
*x = AccessTokensUserSetting_AccessToken{}
mi := &file_store_user_setting_proto_msgTypes[8]
mi := &file_store_user_setting_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -650,7 +778,7 @@ func (x *AccessTokensUserSetting_AccessToken) String() string {
func (*AccessTokensUserSetting_AccessToken) ProtoMessage() {}
func (x *AccessTokensUserSetting_AccessToken) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[8]
mi := &file_store_user_setting_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -680,6 +808,177 @@ func (x *AccessTokensUserSetting_AccessToken) GetDescription() string {
return ""
}
type RefreshTokensUserSetting_RefreshToken struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Unique identifier (matches 'tid' claim in JWT)
TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"`
// When the token expires
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
// When the token was created
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
// Client information for session management UI
ClientInfo *SessionsUserSetting_ClientInfo `protobuf:"bytes,4,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"`
// Optional description
Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokensUserSetting_RefreshToken) Reset() {
*x = RefreshTokensUserSetting_RefreshToken{}
mi := &file_store_user_setting_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RefreshTokensUserSetting_RefreshToken) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokensUserSetting_RefreshToken) ProtoMessage() {}
func (x *RefreshTokensUserSetting_RefreshToken) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RefreshTokensUserSetting_RefreshToken.ProtoReflect.Descriptor instead.
func (*RefreshTokensUserSetting_RefreshToken) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4, 0}
}
func (x *RefreshTokensUserSetting_RefreshToken) GetTokenId() string {
if x != nil {
return x.TokenId
}
return ""
}
func (x *RefreshTokensUserSetting_RefreshToken) GetExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.ExpiresAt
}
return nil
}
func (x *RefreshTokensUserSetting_RefreshToken) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *RefreshTokensUserSetting_RefreshToken) GetClientInfo() *SessionsUserSetting_ClientInfo {
if x != nil {
return x.ClientInfo
}
return nil
}
func (x *RefreshTokensUserSetting_RefreshToken) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
type PersonalAccessTokensUserSetting_PersonalAccessToken struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Unique identifier for this token
TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"`
// SHA-256 hash of the actual token
TokenHash string `protobuf:"bytes,2,opt,name=token_hash,json=tokenHash,proto3" json:"token_hash,omitempty"`
// User-provided description
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
// When the token expires (null = never)
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
// When the token was created
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
// When the token was last used
LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) Reset() {
*x = PersonalAccessTokensUserSetting_PersonalAccessToken{}
mi := &file_store_user_setting_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoMessage() {}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PersonalAccessTokensUserSetting_PersonalAccessToken.ProtoReflect.Descriptor instead.
func (*PersonalAccessTokensUserSetting_PersonalAccessToken) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0}
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenId() string {
if x != nil {
return x.TokenId
}
return ""
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenHash() string {
if x != nil {
return x.TokenHash
}
return ""
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.ExpiresAt
}
return nil
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetLastUsedAt() *timestamppb.Timestamp {
if x != nil {
return x.LastUsedAt
}
return nil
}
type ShortcutsUserSetting_Shortcut struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@ -691,7 +990,7 @@ type ShortcutsUserSetting_Shortcut struct {
func (x *ShortcutsUserSetting_Shortcut) Reset() {
*x = ShortcutsUserSetting_Shortcut{}
mi := &file_store_user_setting_proto_msgTypes[9]
mi := &file_store_user_setting_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -703,7 +1002,7 @@ func (x *ShortcutsUserSetting_Shortcut) String() string {
func (*ShortcutsUserSetting_Shortcut) ProtoMessage() {}
func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[9]
mi := &file_store_user_setting_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -716,7 +1015,7 @@ func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {
// Deprecated: Use ShortcutsUserSetting_Shortcut.ProtoReflect.Descriptor instead.
func (*ShortcutsUserSetting_Shortcut) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{4, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{6, 0}
}
func (x *ShortcutsUserSetting_Shortcut) GetId() string {
@ -754,7 +1053,7 @@ type WebhooksUserSetting_Webhook struct {
func (x *WebhooksUserSetting_Webhook) Reset() {
*x = WebhooksUserSetting_Webhook{}
mi := &file_store_user_setting_proto_msgTypes[10]
mi := &file_store_user_setting_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -766,7 +1065,7 @@ func (x *WebhooksUserSetting_Webhook) String() string {
func (*WebhooksUserSetting_Webhook) ProtoMessage() {}
func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
mi := &file_store_user_setting_proto_msgTypes[10]
mi := &file_store_user_setting_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -779,7 +1078,7 @@ func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebhooksUserSetting_Webhook.ProtoReflect.Descriptor instead.
func (*WebhooksUserSetting_Webhook) Descriptor() ([]byte, []int) {
return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0}
return file_store_user_setting_proto_rawDescGZIP(), []int{7, 0}
}
func (x *WebhooksUserSetting_Webhook) GetId() string {
@ -807,7 +1106,7 @@ var File_store_user_setting_proto protoreflect.FileDescriptor
const file_store_user_setting_proto_rawDesc = "" +
"\n" +
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\x93\x04\n" +
"\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfa\x05\n" +
"\vUserSetting\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x05R\x06userId\x12.\n" +
"\x03key\x18\x02 \x01(\x0e2\x1c.memos.store.UserSetting.KeyR\x03key\x12;\n" +
@ -815,14 +1114,18 @@ const file_store_user_setting_proto_rawDesc = "" +
"\bsessions\x18\x04 \x01(\v2 .memos.store.SessionsUserSettingH\x00R\bsessions\x12K\n" +
"\raccess_tokens\x18\x05 \x01(\v2$.memos.store.AccessTokensUserSettingH\x00R\faccessTokens\x12A\n" +
"\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" +
"\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\"e\n" +
"\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\x12N\n" +
"\x0erefresh_tokens\x18\b \x01(\v2%.memos.store.RefreshTokensUserSettingH\x00R\rrefreshTokens\x12d\n" +
"\x16personal_access_tokens\x18\t \x01(\v2,.memos.store.PersonalAccessTokensUserSettingH\x00R\x14personalAccessTokens\"\x95\x01\n" +
"\x03Key\x12\x13\n" +
"\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" +
"\aGENERAL\x10\x01\x12\f\n" +
"\bSESSIONS\x10\x02\x12\x11\n" +
"\rACCESS_TOKENS\x10\x03\x12\r\n" +
"\tSHORTCUTS\x10\x04\x12\f\n" +
"\bWEBHOOKS\x10\x05B\a\n" +
"\bWEBHOOKS\x10\x05\x12\x12\n" +
"\x0eREFRESH_TOKENS\x10\x06\x12\x1a\n" +
"\x16PERSONAL_ACCESS_TOKENS\x10\aB\a\n" +
"\x05value\"k\n" +
"\x12GeneralUserSetting\x12\x16\n" +
"\x06locale\x18\x01 \x01(\tR\x06locale\x12'\n" +
@ -852,7 +1155,31 @@ const file_store_user_setting_proto_rawDesc = "" +
"\raccess_tokens\x18\x01 \x03(\v20.memos.store.AccessTokensUserSetting.AccessTokenR\faccessTokens\x1aR\n" +
"\vAccessToken\x12!\n" +
"\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\"\xaa\x01\n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\"\x87\x03\n" +
"\x18RefreshTokensUserSetting\x12Y\n" +
"\x0erefresh_tokens\x18\x01 \x03(\v22.memos.store.RefreshTokensUserSetting.RefreshTokenR\rrefreshTokens\x1a\x8f\x02\n" +
"\fRefreshToken\x12\x19\n" +
"\btoken_id\x18\x01 \x01(\tR\atokenId\x129\n" +
"\n" +
"expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x129\n" +
"\n" +
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12L\n" +
"\vclient_info\x18\x04 \x01(\v2+.memos.store.SessionsUserSetting.ClientInfoR\n" +
"clientInfo\x12 \n" +
"\vdescription\x18\x05 \x01(\tR\vdescription\"\xa3\x03\n" +
"\x1fPersonalAccessTokensUserSetting\x12X\n" +
"\x06tokens\x18\x01 \x03(\v2@.memos.store.PersonalAccessTokensUserSetting.PersonalAccessTokenR\x06tokens\x1a\xa5\x02\n" +
"\x13PersonalAccessToken\x12\x19\n" +
"\btoken_id\x18\x01 \x01(\tR\atokenId\x12\x1d\n" +
"\n" +
"token_hash\x18\x02 \x01(\tR\ttokenHash\x12 \n" +
"\vdescription\x18\x03 \x01(\tR\vdescription\x129\n" +
"\n" +
"expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x129\n" +
"\n" +
"created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12<\n" +
"\flast_used_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"lastUsedAt\"\xaa\x01\n" +
"\x14ShortcutsUserSetting\x12H\n" +
"\tshortcuts\x18\x01 \x03(\v2*.memos.store.ShortcutsUserSetting.ShortcutR\tshortcuts\x1aH\n" +
"\bShortcut\x12\x0e\n" +
@ -880,41 +1207,55 @@ func file_store_user_setting_proto_rawDescGZIP() []byte {
}
var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
var file_store_user_setting_proto_goTypes = []any{
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
(*UserSetting)(nil), // 1: memos.store.UserSetting
(*GeneralUserSetting)(nil), // 2: memos.store.GeneralUserSetting
(*SessionsUserSetting)(nil), // 3: memos.store.SessionsUserSetting
(*AccessTokensUserSetting)(nil), // 4: memos.store.AccessTokensUserSetting
(*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting
(*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting
(*SessionsUserSetting_Session)(nil), // 7: memos.store.SessionsUserSetting.Session
(*SessionsUserSetting_ClientInfo)(nil), // 8: memos.store.SessionsUserSetting.ClientInfo
(*AccessTokensUserSetting_AccessToken)(nil), // 9: memos.store.AccessTokensUserSetting.AccessToken
(*ShortcutsUserSetting_Shortcut)(nil), // 10: memos.store.ShortcutsUserSetting.Shortcut
(*WebhooksUserSetting_Webhook)(nil), // 11: memos.store.WebhooksUserSetting.Webhook
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
(UserSetting_Key)(0), // 0: memos.store.UserSetting.Key
(*UserSetting)(nil), // 1: memos.store.UserSetting
(*GeneralUserSetting)(nil), // 2: memos.store.GeneralUserSetting
(*SessionsUserSetting)(nil), // 3: memos.store.SessionsUserSetting
(*AccessTokensUserSetting)(nil), // 4: memos.store.AccessTokensUserSetting
(*RefreshTokensUserSetting)(nil), // 5: memos.store.RefreshTokensUserSetting
(*PersonalAccessTokensUserSetting)(nil), // 6: memos.store.PersonalAccessTokensUserSetting
(*ShortcutsUserSetting)(nil), // 7: memos.store.ShortcutsUserSetting
(*WebhooksUserSetting)(nil), // 8: memos.store.WebhooksUserSetting
(*SessionsUserSetting_Session)(nil), // 9: memos.store.SessionsUserSetting.Session
(*SessionsUserSetting_ClientInfo)(nil), // 10: memos.store.SessionsUserSetting.ClientInfo
(*AccessTokensUserSetting_AccessToken)(nil), // 11: memos.store.AccessTokensUserSetting.AccessToken
(*RefreshTokensUserSetting_RefreshToken)(nil), // 12: memos.store.RefreshTokensUserSetting.RefreshToken
(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 13: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
(*ShortcutsUserSetting_Shortcut)(nil), // 14: memos.store.ShortcutsUserSetting.Shortcut
(*WebhooksUserSetting_Webhook)(nil), // 15: memos.store.WebhooksUserSetting.Webhook
(*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp
}
var file_store_user_setting_proto_depIdxs = []int32{
0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key
2, // 1: memos.store.UserSetting.general:type_name -> memos.store.GeneralUserSetting
3, // 2: memos.store.UserSetting.sessions:type_name -> memos.store.SessionsUserSetting
4, // 3: memos.store.UserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting
5, // 4: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting
6, // 5: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting
7, // 6: memos.store.SessionsUserSetting.sessions:type_name -> memos.store.SessionsUserSetting.Session
9, // 7: memos.store.AccessTokensUserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting.AccessToken
10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
12, // 10: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp
12, // 11: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp
8, // 12: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
7, // 4: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting
8, // 5: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting
5, // 6: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting
6, // 7: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting
9, // 8: memos.store.SessionsUserSetting.sessions:type_name -> memos.store.SessionsUserSetting.Session
11, // 9: memos.store.AccessTokensUserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting.AccessToken
12, // 10: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken
13, // 11: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken
14, // 12: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut
15, // 13: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook
16, // 14: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp
16, // 15: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp
10, // 16: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo
16, // 17: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp
16, // 18: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp
10, // 19: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo
16, // 20: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
16, // 21: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
16, // 22: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
23, // [23:23] is the sub-list for method output_type
23, // [23:23] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
}
func init() { file_store_user_setting_proto_init() }
@ -928,6 +1269,8 @@ func file_store_user_setting_proto_init() {
(*UserSetting_AccessTokens)(nil),
(*UserSetting_Shortcuts)(nil),
(*UserSetting_Webhooks)(nil),
(*UserSetting_RefreshTokens)(nil),
(*UserSetting_PersonalAccessTokens)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@ -935,7 +1278,7 @@ func file_store_user_setting_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),
NumEnums: 1,
NumMessages: 11,
NumMessages: 15,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -19,6 +19,10 @@ message UserSetting {
SHORTCUTS = 4;
// The webhooks of the user.
WEBHOOKS = 5;
// Refresh tokens for the user.
REFRESH_TOKENS = 6;
// Personal access tokens for the user.
PERSONAL_ACCESS_TOKENS = 7;
}
int32 user_id = 1;
@ -30,6 +34,8 @@ message UserSetting {
AccessTokensUserSetting access_tokens = 5;
ShortcutsUserSetting shortcuts = 6;
WebhooksUserSetting webhooks = 7;
RefreshTokensUserSetting refresh_tokens = 8;
PersonalAccessTokensUserSetting personal_access_tokens = 9;
}
}
@ -83,6 +89,40 @@ message AccessTokensUserSetting {
repeated AccessToken access_tokens = 1;
}
message RefreshTokensUserSetting {
message RefreshToken {
// Unique identifier (matches 'tid' claim in JWT)
string token_id = 1;
// When the token expires
google.protobuf.Timestamp expires_at = 2;
// When the token was created
google.protobuf.Timestamp created_at = 3;
// Client information for session management UI
SessionsUserSetting.ClientInfo client_info = 4;
// Optional description
string description = 5;
}
repeated RefreshToken refresh_tokens = 1;
}
message PersonalAccessTokensUserSetting {
message PersonalAccessToken {
// Unique identifier for this token
string token_id = 1;
// SHA-256 hash of the actual token
string token_hash = 2;
// User-provided description
string description = 3;
// When the token expires (null = never)
google.protobuf.Timestamp expires_at = 4;
// When the token was created
google.protobuf.Timestamp created_at = 5;
// When the token was last used
google.protobuf.Timestamp last_used_at = 6;
}
repeated PersonalAccessToken tokens = 1;
}
message ShortcutsUserSetting {
message Shortcut {
string id = 1;

View File

@ -2,9 +2,10 @@ package auth
import (
"context"
"log/slog"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"
"google.golang.org/protobuf/types/known/timestamppb"
@ -35,182 +36,128 @@ func NewAuthenticator(store *store.Store, secret string) *Authenticator {
}
}
// AuthenticateBySession validates a session cookie and returns the authenticated user.
//
// Validation steps:
// 1. Use session ID to find the user and session details (single DB query)
// 2. Verify user exists and is not archived
// 3. Check session hasn't expired (sliding expiration: 14 days from last access)
//
// Returns the user if authentication succeeds, or an error describing the failure.
func (a *Authenticator) AuthenticateBySession(ctx context.Context, sessionID string) (*store.User, error) {
if sessionID == "" {
return nil, errors.New("session ID not found")
}
// Find the session and user in a single database query
result, err := a.store.GetUserSessionByID(ctx, sessionID)
// AuthenticateByAccessTokenV2 validates a short-lived access token.
// Returns claims without database query (stateless validation).
func (a *Authenticator) AuthenticateByAccessTokenV2(accessToken string) (*UserClaims, error) {
claims, err := ParseAccessTokenV2(accessToken, []byte(a.secret))
if err != nil {
return nil, errors.Wrap(err, "session not found")
}
user, err := a.store.GetUser(ctx, &store.FindUser{ID: &result.UserID})
if err != nil {
return nil, errors.Wrap(err, "failed to get user")
}
if user == nil {
return nil, errors.New("user not found")
}
if user.RowStatus == store.Archived {
return nil, errors.New("user is archived")
}
// Validate session expiration
if result.Session.LastAccessedTime != nil {
expiration := result.Session.LastAccessedTime.AsTime().Add(SessionSlidingDuration)
if expiration.Before(time.Now()) {
return nil, errors.New("session expired")
}
}
return user, nil
}
// AuthenticateByJWT validates a JWT access token and returns the authenticated user.
//
// Validation steps:
// 1. Parse and verify JWT signature using server secret
// 2. Verify key ID matches expected version
// 3. Extract user ID from JWT claims (subject field)
// 4. Verify user exists and is not archived
// 5. Verify token exists in user's access_tokens list (for revocation support)
//
// Returns the user if authentication succeeds, or an error describing the failure.
func (a *Authenticator) AuthenticateByJWT(ctx context.Context, accessToken string) (*store.User, error) {
if accessToken == "" {
return nil, errors.New("access token not found")
}
claims := &ClaimsMessage{}
_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.Errorf("unexpected signing method: %v", t.Header["alg"])
}
kid, ok := t.Header["kid"].(string)
if !ok || kid != KeyID {
return nil, errors.Errorf("unexpected kid: %v", t.Header["kid"])
}
return []byte(a.secret), nil
})
if err != nil {
return nil, errors.New("invalid or expired access token")
return nil, errors.Wrap(err, "invalid access token")
}
userID, err := util.ConvertStringToInt32(claims.Subject)
if err != nil {
return nil, errors.Wrap(err, "malformed ID in token")
return nil, errors.Wrap(err, "invalid user ID in token")
}
return &UserClaims{
UserID: userID,
Username: claims.Username,
Role: claims.Role,
Status: claims.Status,
}, nil
}
// AuthenticateByRefreshToken validates a refresh token against the database.
func (a *Authenticator) AuthenticateByRefreshToken(ctx context.Context, refreshToken string) (*store.User, string, error) {
claims, err := ParseRefreshToken(refreshToken, []byte(a.secret))
if err != nil {
return nil, "", errors.Wrap(err, "invalid refresh token")
}
userID, err := util.ConvertStringToInt32(claims.Subject)
if err != nil {
return nil, "", errors.Wrap(err, "invalid user ID in token")
}
// Check token exists in database (revocation check)
token, err := a.store.GetUserRefreshTokenByID(ctx, userID, claims.TokenID)
if err != nil {
return nil, "", errors.Wrap(err, "failed to get refresh token")
}
if token == nil {
return nil, "", errors.New("refresh token revoked")
}
// Check token not expired
if token.ExpiresAt != nil && token.ExpiresAt.AsTime().Before(time.Now()) {
return nil, "", errors.New("refresh token expired")
}
// Get user
user, err := a.store.GetUser(ctx, &store.FindUser{ID: &userID})
if err != nil {
return nil, errors.Wrap(err, "failed to get user")
return nil, "", errors.Wrap(err, "failed to get user")
}
if user == nil {
return nil, errors.Errorf("user %d not found", userID)
return nil, "", errors.New("user not found")
}
if user.RowStatus == store.Archived {
return nil, errors.Errorf("user %d is archived", userID)
return nil, "", errors.New("user is archived")
}
accessTokens, err := a.store.GetUserAccessTokens(ctx, user.ID)
return user, claims.TokenID, nil
}
// AuthenticateByPAT validates a Personal Access Token.
func (a *Authenticator) AuthenticateByPAT(ctx context.Context, token string) (*store.User, *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) {
if !strings.HasPrefix(token, PersonalAccessTokenPrefix) {
return nil, nil, errors.New("invalid PAT format")
}
tokenHash := HashPersonalAccessToken(token)
result, err := a.store.GetUserByPATHash(ctx, tokenHash)
if err != nil {
return nil, errors.Wrap(err, "failed to get user access tokens")
}
if !validateAccessToken(accessToken, accessTokens) {
return nil, errors.New("invalid access token")
return nil, nil, errors.Wrap(err, "invalid PAT")
}
return user, nil
}
// AuthorizeAndSetContext checks user authorization for the given procedure and sets context values.
//
// Authorization checks:
// - Admin-only methods require Host or Admin role (checked via isAdminOnly function)
//
// Context values set:
// - UserIDContextKey: Always set with the user's ID
// - SessionIDContextKey: Set if authenticated via session cookie
// - AccessTokenContextKey: Set if authenticated via JWT token
//
// Also updates session last accessed time for session-based auth (sliding expiration).
//
// Returns the updated context or an error if authorization fails.
func (a *Authenticator) AuthorizeAndSetContext(ctx context.Context, procedure string, user *store.User, sessionID, accessToken string, isAdminOnly func(string) bool) (context.Context, error) {
// Check admin-only method authorization
if isAdminOnly != nil && isAdminOnly(procedure) && user.Role != store.RoleHost && user.Role != store.RoleAdmin {
return nil, errors.Errorf("user %q is not authorized for this operation", user.Username)
// Check expiry
if result.PAT.ExpiresAt != nil && result.PAT.ExpiresAt.AsTime().Before(time.Now()) {
return nil, nil, errors.New("PAT expired")
}
// Set user ID in context (always)
ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
// Set authentication method specific context values
if sessionID != "" {
ctx = context.WithValue(ctx, SessionIDContextKey, sessionID)
// Update session last accessed time for sliding expiration
_ = a.store.UpdateUserSessionLastAccessed(ctx, user.ID, sessionID, timestamppb.Now())
} else if accessToken != "" {
ctx = context.WithValue(ctx, AccessTokenContextKey, accessToken)
// Check user status
if result.User.RowStatus == store.Archived {
return nil, nil, errors.New("user is archived")
}
return ctx, nil
}
// validateAccessToken checks if the token exists in the user's access tokens list.
// This enables token revocation: deleted tokens are removed from the list.
func validateAccessToken(token string, tokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
for _, t := range tokens {
if token == t.AccessToken {
return true
}
}
return false
}
// UpdateSessionLastAccessed updates the last accessed time for a session.
// This implements sliding expiration - sessions remain valid as long as they're used.
// Should be called after successful session-based authentication.
func (a *Authenticator) UpdateSessionLastAccessed(ctx context.Context, userID int32, sessionID string) {
// Fire-and-forget update; failures are logged but don't block the request
_ = a.store.UpdateUserSessionLastAccessed(ctx, userID, sessionID, timestamppb.Now())
return result.User, result.PAT, nil
}
// AuthResult contains the result of an authentication attempt.
type AuthResult struct {
User *store.User
SessionID string // Non-empty if authenticated via session cookie
AccessToken string // Non-empty if authenticated via JWT
User *store.User // Set for PAT and legacy auth
Claims *UserClaims // Set for Access Token V2 (stateless)
SessionID string // Non-empty if authenticated via session cookie
AccessToken string // Non-empty if authenticated via JWT
}
// Authenticate tries to authenticate using the provided credentials.
// It tries session cookie first, then JWT token.
// Priority: 1. Access Token V2, 2. PAT
// Returns nil if no valid credentials are provided.
// On successful session auth, it also updates the session sliding expiration.
func (a *Authenticator) Authenticate(ctx context.Context, sessionID, authHeader string) *AuthResult {
// Try session cookie authentication first
if sessionID != "" {
user, err := a.AuthenticateBySession(ctx, sessionID)
if err == nil && user != nil {
a.UpdateSessionLastAccessed(ctx, user.ID, sessionID)
return &AuthResult{User: user, SessionID: sessionID}
func (a *Authenticator) Authenticate(ctx context.Context, _, authHeader string) *AuthResult {
token := ExtractBearerToken(authHeader)
// Try Access Token V2 (stateless)
if token != "" && !strings.HasPrefix(token, PersonalAccessTokenPrefix) {
claims, err := a.AuthenticateByAccessTokenV2(token)
if err == nil && claims != nil {
return &AuthResult{
Claims: claims,
AccessToken: token,
}
}
}
// Try JWT token authentication
if token := ExtractBearerToken(authHeader); token != "" {
user, err := a.AuthenticateByJWT(ctx, token)
// Try PAT
if token != "" && strings.HasPrefix(token, PersonalAccessTokenPrefix) {
user, pat, err := a.AuthenticateByPAT(ctx, token)
if err == nil && user != nil {
// Update last used (fire-and-forget with logging)
go func() {
if err := a.store.UpdatePATLastUsed(context.Background(), user.ID, pat.TokenId, timestamppb.Now()); err != nil {
slog.Warn("failed to update PAT last used time", "error", err, "userID", user.ID)
}
}()
return &AuthResult{User: user, AccessToken: token}
}
}

View File

@ -23,6 +23,12 @@ const (
// AccessTokenContextKey stores the JWT token for token-based auth.
// Only set when authenticated via Bearer token.
AccessTokenContextKey
// UserClaimsContextKey stores the claims from access token.
UserClaimsContextKey
// RefreshTokenIDContextKey stores the refresh token ID.
RefreshTokenIDContextKey
)
// GetUserID retrieves the authenticated user's ID from the context.
@ -70,3 +76,25 @@ func SetUserInContext(ctx context.Context, user *store.User, sessionID, accessTo
}
return ctx
}
// UserClaims represents authenticated user info from access token.
type UserClaims struct {
UserID int32
Username string
Role string
Status string
}
// GetUserClaims retrieves the user claims from context.
// Returns nil if not authenticated via access token.
func GetUserClaims(ctx context.Context) *UserClaims {
if v, ok := ctx.Value(UserClaimsContextKey).(*UserClaims); ok {
return v
}
return nil
}
// SetUserClaimsInContext sets the user claims in context.
func SetUserClaimsInContext(ctx context.Context, claims *UserClaims) context.Context {
return context.WithValue(ctx, UserClaimsContextKey, claims)
}

View File

@ -33,3 +33,16 @@ func ExtractBearerToken(authHeader string) string {
}
return parts[1]
}
// ExtractRefreshTokenFromCookie extracts the refresh token from cookie header.
func ExtractRefreshTokenFromCookie(cookieHeader string) string {
if cookieHeader == "" {
return ""
}
req := &http.Request{Header: http.Header{"Cookie": []string{cookieHeader}}}
cookie, err := req.Cookie(RefreshTokenCookieName)
if err != nil {
return ""
}
return cookie.Value
}

View File

@ -10,10 +10,13 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"
"github.com/usememos/memos/internal/util"
)
@ -40,6 +43,21 @@ const (
// SessionCookieName is the HTTP cookie name used to store session information.
// Cookie value is the session ID (UUID).
SessionCookieName = "user_session"
// AccessTokenDuration is the lifetime of access tokens (15 minutes).
AccessTokenDuration = 15 * time.Minute
// RefreshTokenDuration is the lifetime of refresh tokens (30 days).
RefreshTokenDuration = 30 * 24 * time.Hour
// RefreshTokenAudienceName is the audience claim for refresh tokens.
RefreshTokenAudienceName = "user.refresh-token"
// RefreshTokenCookieName is the cookie name for refresh tokens.
RefreshTokenCookieName = "memos_refresh"
// PersonalAccessTokenPrefix is the prefix for PAT tokens.
PersonalAccessTokenPrefix = "memos_pat_"
)
// ClaimsMessage represents the claims structure in a JWT token.
@ -56,6 +74,24 @@ type ClaimsMessage struct {
jwt.RegisteredClaims
}
// AccessTokenClaims contains claims for short-lived access tokens.
// These tokens are validated by signature only (stateless).
type AccessTokenClaims struct {
Type string `json:"type"` // "access"
Role string `json:"role"` // User role
Status string `json:"status"` // User status
Username string `json:"username"` // Username for display
jwt.RegisteredClaims
}
// RefreshTokenClaims contains claims for long-lived refresh tokens.
// These tokens are validated against the database for revocation.
type RefreshTokenClaims struct {
Type string `json:"type"` // "refresh"
TokenID string `json:"tid"` // Token ID for revocation lookup
jwt.RegisteredClaims
}
// GenerateAccessToken generates a JWT access token for a user.
//
// Parameters:
@ -110,3 +146,121 @@ func generateToken(username string, userID int32, audience string, expirationTim
func GenerateSessionID() string {
return util.GenUUID()
}
// GenerateAccessTokenV2 generates a short-lived access token with user claims.
func GenerateAccessTokenV2(userID int32, username, role, status string, secret []byte) (string, time.Time, error) {
expiresAt := time.Now().Add(AccessTokenDuration)
claims := &AccessTokenClaims{
Type: "access",
Role: role,
Status: status,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: Issuer,
Audience: jwt.ClaimStrings{AccessTokenAudienceName},
Subject: fmt.Sprint(userID),
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token.Header["kid"] = KeyID
tokenString, err := token.SignedString(secret)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GenerateRefreshToken generates a long-lived refresh token.
func GenerateRefreshToken(userID int32, tokenID string, secret []byte) (string, time.Time, error) {
expiresAt := time.Now().Add(RefreshTokenDuration)
claims := &RefreshTokenClaims{
Type: "refresh",
TokenID: tokenID,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: Issuer,
Audience: jwt.ClaimStrings{RefreshTokenAudienceName},
Subject: fmt.Sprint(userID),
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token.Header["kid"] = KeyID
tokenString, err := token.SignedString(secret)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// GeneratePersonalAccessToken generates a random PAT string.
func GeneratePersonalAccessToken() string {
randomStr, err := util.RandomString(32)
if err != nil {
// Fallback to UUID if RandomString fails
return PersonalAccessTokenPrefix + util.GenUUID()
}
return PersonalAccessTokenPrefix + randomStr
}
// HashPersonalAccessToken returns SHA-256 hash of a PAT.
func HashPersonalAccessToken(token string) string {
hash := sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:])
}
// verifyJWTKeyFunc returns a jwt.Keyfunc that validates the signing method and key ID.
func verifyJWTKeyFunc(secret []byte) jwt.Keyfunc {
return func(t *jwt.Token) (any, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.Errorf("unexpected signing method: %v", t.Header["alg"])
}
kid, ok := t.Header["kid"].(string)
if !ok || kid != KeyID {
return nil, errors.Errorf("unexpected kid: %v", t.Header["kid"])
}
return secret, nil
}
}
// ParseAccessTokenV2 parses and validates a short-lived access token.
func ParseAccessTokenV2(tokenString string, secret []byte) (*AccessTokenClaims, error) {
claims := &AccessTokenClaims{}
_, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret),
jwt.WithIssuer(Issuer),
jwt.WithAudience(AccessTokenAudienceName),
)
if err != nil {
return nil, err
}
if claims.Type != "access" {
return nil, errors.New("invalid token type: expected access token")
}
return claims, nil
}
// ParseRefreshToken parses and validates a refresh token.
func ParseRefreshToken(tokenString string, secret []byte) (*RefreshTokenClaims, error) {
claims := &RefreshTokenClaims{}
_, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret),
jwt.WithIssuer(Issuer),
jwt.WithAudience(RefreshTokenAudienceName),
)
if err != nil {
return nil, err
}
if claims.Type != "refresh" {
return nil, errors.New("invalid token type: expected refresh token")
}
return claims, nil
}

306
server/auth/token_test.go Normal file
View File

@ -0,0 +1,306 @@
package auth
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateAccessTokenV2(t *testing.T) {
secret := []byte("test-secret")
t.Run("generates valid access token", func(t *testing.T) {
token, expiresAt, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
assert.NotEmpty(t, token)
assert.True(t, expiresAt.After(time.Now()))
assert.True(t, expiresAt.Before(time.Now().Add(AccessTokenDuration+time.Minute)))
})
t.Run("generates different tokens for same user", func(t *testing.T) {
token1, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
time.Sleep(2 * time.Second) // Ensure different timestamps (tokens have 1s precision)
token2, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
assert.NotEqual(t, token1, token2, "tokens should be different due to different timestamps")
})
}
func TestParseAccessTokenV2(t *testing.T) {
secret := []byte("test-secret")
t.Run("parses valid access token", func(t *testing.T) {
token, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
claims, err := ParseAccessTokenV2(token, secret)
require.NoError(t, err)
assert.Equal(t, "1", claims.Subject)
assert.Equal(t, "testuser", claims.Username)
assert.Equal(t, "USER", claims.Role)
assert.Equal(t, "ACTIVE", claims.Status)
assert.Equal(t, "access", claims.Type)
})
t.Run("fails with wrong secret", func(t *testing.T) {
token, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
wrongSecret := []byte("wrong-secret")
_, err = ParseAccessTokenV2(token, wrongSecret)
assert.Error(t, err)
})
t.Run("fails with invalid token", func(t *testing.T) {
_, err := ParseAccessTokenV2("invalid-token", secret)
assert.Error(t, err)
})
t.Run("fails with refresh token", func(t *testing.T) {
// Generate a refresh token and try to parse it as access token
// Should fail because audience mismatch is caught before type check
refreshToken, _, err := GenerateRefreshToken(1, "token-id", secret)
require.NoError(t, err)
_, err = ParseAccessTokenV2(refreshToken, secret)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid audience")
})
t.Run("parses token with different roles", func(t *testing.T) {
roles := []string{"USER", "ADMIN", "HOST"}
for _, role := range roles {
token, _, err := GenerateAccessTokenV2(1, "testuser", role, "ACTIVE", secret)
require.NoError(t, err)
claims, err := ParseAccessTokenV2(token, secret)
require.NoError(t, err)
assert.Equal(t, role, claims.Role)
}
})
}
func TestGenerateRefreshToken(t *testing.T) {
secret := []byte("test-secret")
t.Run("generates valid refresh token", func(t *testing.T) {
token, expiresAt, err := GenerateRefreshToken(1, "token-id-123", secret)
require.NoError(t, err)
assert.NotEmpty(t, token)
assert.True(t, expiresAt.After(time.Now().Add(29*24*time.Hour)))
})
t.Run("generates different tokens for different token IDs", func(t *testing.T) {
token1, _, err := GenerateRefreshToken(1, "token-id-1", secret)
require.NoError(t, err)
token2, _, err := GenerateRefreshToken(1, "token-id-2", secret)
require.NoError(t, err)
assert.NotEqual(t, token1, token2)
})
}
func TestParseRefreshToken(t *testing.T) {
secret := []byte("test-secret")
t.Run("parses valid refresh token", func(t *testing.T) {
token, _, err := GenerateRefreshToken(1, "token-id-123", secret)
require.NoError(t, err)
claims, err := ParseRefreshToken(token, secret)
require.NoError(t, err)
assert.Equal(t, "1", claims.Subject)
assert.Equal(t, "token-id-123", claims.TokenID)
assert.Equal(t, "refresh", claims.Type)
})
t.Run("fails with wrong secret", func(t *testing.T) {
token, _, err := GenerateRefreshToken(1, "token-id-123", secret)
require.NoError(t, err)
wrongSecret := []byte("wrong-secret")
_, err = ParseRefreshToken(token, wrongSecret)
assert.Error(t, err)
})
t.Run("fails with invalid token", func(t *testing.T) {
_, err := ParseRefreshToken("invalid-token", secret)
assert.Error(t, err)
})
t.Run("fails with access token", func(t *testing.T) {
// Generate an access token and try to parse it as refresh token
// Should fail because audience mismatch is caught before type check
accessToken, _, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
_, err = ParseRefreshToken(accessToken, secret)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid audience")
})
}
func TestGeneratePersonalAccessToken(t *testing.T) {
t.Run("generates token with correct prefix", func(t *testing.T) {
token := GeneratePersonalAccessToken()
assert.NotEmpty(t, token)
assert.True(t, len(token) > len(PersonalAccessTokenPrefix))
assert.Equal(t, PersonalAccessTokenPrefix, token[:len(PersonalAccessTokenPrefix)])
})
t.Run("generates unique tokens", func(t *testing.T) {
token1 := GeneratePersonalAccessToken()
token2 := GeneratePersonalAccessToken()
assert.NotEqual(t, token1, token2)
})
t.Run("generates token of sufficient length", func(t *testing.T) {
token := GeneratePersonalAccessToken()
// Prefix is "memos_pat_" (10 chars) + 32 random chars = at least 42 chars
assert.True(t, len(token) >= 42, "token should be at least 42 characters")
})
}
func TestHashPersonalAccessToken(t *testing.T) {
t.Run("generates SHA-256 hash", func(t *testing.T) {
token := "memos_pat_abc123"
hash := HashPersonalAccessToken(token)
assert.NotEmpty(t, hash)
assert.Len(t, hash, 64, "SHA-256 hex should be 64 characters")
})
t.Run("same input produces same hash", func(t *testing.T) {
token := "memos_pat_abc123"
hash1 := HashPersonalAccessToken(token)
hash2 := HashPersonalAccessToken(token)
assert.Equal(t, hash1, hash2)
})
t.Run("different inputs produce different hashes", func(t *testing.T) {
token1 := "memos_pat_abc123"
token2 := "memos_pat_xyz789"
hash1 := HashPersonalAccessToken(token1)
hash2 := HashPersonalAccessToken(token2)
assert.NotEqual(t, hash1, hash2)
})
t.Run("hash is deterministic", func(t *testing.T) {
token := GeneratePersonalAccessToken()
hash1 := HashPersonalAccessToken(token)
hash2 := HashPersonalAccessToken(token)
assert.Equal(t, hash1, hash2)
})
}
func TestAccessTokenV2Integration(t *testing.T) {
secret := []byte("test-secret")
t.Run("full lifecycle: generate, parse, validate", func(t *testing.T) {
userID := int32(42)
username := "john_doe"
role := "ADMIN"
status := "ACTIVE"
// Generate token
token, expiresAt, err := GenerateAccessTokenV2(userID, username, role, status, secret)
require.NoError(t, err)
assert.NotEmpty(t, token)
// Parse token
claims, err := ParseAccessTokenV2(token, secret)
require.NoError(t, err)
// Validate claims
assert.Equal(t, "42", claims.Subject)
assert.Equal(t, username, claims.Username)
assert.Equal(t, role, claims.Role)
assert.Equal(t, status, claims.Status)
assert.Equal(t, "access", claims.Type)
assert.Equal(t, Issuer, claims.Issuer)
assert.NotNil(t, claims.IssuedAt)
assert.NotNil(t, claims.ExpiresAt)
// Validate expiration
assert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt))
})
}
func TestRefreshTokenIntegration(t *testing.T) {
secret := []byte("test-secret")
t.Run("full lifecycle: generate, parse, validate", func(t *testing.T) {
userID := int32(42)
tokenID := "unique-token-id-456"
// Generate token
token, expiresAt, err := GenerateRefreshToken(userID, tokenID, secret)
require.NoError(t, err)
assert.NotEmpty(t, token)
// Parse token
claims, err := ParseRefreshToken(token, secret)
require.NoError(t, err)
// Validate claims
assert.Equal(t, "42", claims.Subject)
assert.Equal(t, tokenID, claims.TokenID)
assert.Equal(t, "refresh", claims.Type)
assert.Equal(t, Issuer, claims.Issuer)
assert.NotNil(t, claims.IssuedAt)
assert.NotNil(t, claims.ExpiresAt)
// Validate expiration
assert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt))
})
}
func TestPersonalAccessTokenIntegration(t *testing.T) {
t.Run("full lifecycle: generate, hash, verify", func(t *testing.T) {
// Generate token
token := GeneratePersonalAccessToken()
assert.NotEmpty(t, token)
assert.True(t, len(token) > len(PersonalAccessTokenPrefix))
// Hash token
hash := HashPersonalAccessToken(token)
assert.Len(t, hash, 64)
// Verify same token produces same hash
hashAgain := HashPersonalAccessToken(token)
assert.Equal(t, hash, hashAgain)
// Verify different token produces different hash
token2 := GeneratePersonalAccessToken()
hash2 := HashPersonalAccessToken(token2)
assert.NotEqual(t, hash, hash2)
})
}
func TestTokenExpiration(t *testing.T) {
secret := []byte("test-secret")
t.Run("access token expires after AccessTokenDuration", func(t *testing.T) {
_, expiresAt, err := GenerateAccessTokenV2(1, "testuser", "USER", "ACTIVE", secret)
require.NoError(t, err)
expectedExpiry := time.Now().Add(AccessTokenDuration)
delta := expiresAt.Sub(expectedExpiry)
assert.True(t, delta < time.Second, "expiration should be within 1 second of expected")
})
t.Run("refresh token expires after RefreshTokenDuration", func(t *testing.T) {
_, expiresAt, err := GenerateRefreshToken(1, "token-id", secret)
require.NoError(t, err)
expectedExpiry := time.Now().Add(RefreshTokenDuration)
delta := expiresAt.Sub(expectedExpiry)
assert.True(t, delta < time.Second, "expiration should be within 1 second of expected")
})
}

View File

@ -9,9 +9,9 @@ package v1
// Format: Full gRPC procedure path as returned by req.Spec().Procedure (Connect)
// or info.FullMethod (gRPC interceptor).
var PublicMethods = map[string]struct{}{
// Auth Service - login flow must be accessible without auth
"/memos.api.v1.AuthService/CreateSession": {},
"/memos.api.v1.AuthService/GetCurrentSession": {},
// Auth Service - login/token endpoints must be accessible without auth
"/memos.api.v1.AuthService/SignIn": {},
"/memos.api.v1.AuthService/RefreshToken": {}, // Token refresh uses cookie, must be accessible when access token expired
// Instance Service - needed before login to show instance info
"/memos.api.v1.InstanceService/GetInstanceProfile": {},

View File

@ -10,8 +10,8 @@ import (
func TestPublicMethodsArePublic(t *testing.T) {
publicMethods := []string{
// Auth Service
"/memos.api.v1.AuthService/CreateSession",
"/memos.api.v1.AuthService/GetCurrentSession",
"/memos.api.v1.AuthService/SignIn",
"/memos.api.v1.AuthService/RefreshToken",
// Instance Service
"/memos.api.v1.InstanceService/GetInstanceProfile",
"/memos.api.v1.InstanceService/GetInstanceSetting",
@ -39,8 +39,9 @@ func TestPublicMethodsArePublic(t *testing.T) {
// TestProtectedMethodsRequireAuth verifies that non-public methods are recognized as protected.
func TestProtectedMethodsRequireAuth(t *testing.T) {
protectedMethods := []string{
// Auth Service - logout requires auth
"/memos.api.v1.AuthService/DeleteSession",
// Auth Service - logout and get current user require auth
"/memos.api.v1.AuthService/SignOut",
"/memos.api.v1.AuthService/GetCurrentUser",
// Instance Service - admin operations
"/memos.api.v1.InstanceService/UpdateInstanceSetting",
// User Service - modification operations

View File

@ -43,7 +43,7 @@ var SupportedThumbnailMimeTypes = []string{
}
func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.CreateAttachmentRequest) (*v1pb.Attachment, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -123,7 +123,7 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
}
func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAttachmentsRequest) (*v1pb.ListAttachmentsResponse, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -234,7 +234,7 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err)
}
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -29,17 +29,13 @@ const (
unmatchedUsernameAndPasswordError = "unmatched username and password"
)
// GetCurrentSession retrieves the current authenticated session information.
// GetCurrentUser returns the authenticated user's information.
// Validates the access token and returns user details.
//
// 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)
// Authentication: Required (access token).
// Returns: User information.
func (s *APIV1Service) GetCurrentUser(ctx context.Context, _ *v1pb.GetCurrentUserRequest) (*v1pb.GetCurrentUserResponse, error) {
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
}
@ -51,37 +47,21 @@ func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrent
return nil, status.Errorf(codes.Unauthenticated, "user not found")
}
var lastAccessedAt *timestamppb.Timestamp
// Update session last accessed time if we have a session ID and get the current session info
if sessionID := auth.GetSessionID(ctx); sessionID != "" {
now := timestamppb.Now()
if err := s.Store.UpdateUserSessionLastAccessed(ctx, user.ID, sessionID, now); err != nil {
// Log error but don't fail the request
slog.Error("failed to update session last accessed time", "error", err)
}
lastAccessedAt = now
}
return &v1pb.GetCurrentSessionResponse{
User: convertUserFromStore(user),
LastAccessedAt: lastAccessedAt,
return &v1pb.GetCurrentUserResponse{
User: convertUserFromStore(user),
}, nil
}
// CreateSession authenticates a user and establishes a new session.
// SignIn authenticates a user with credentials and returns tokens.
// On success, returns an access token and sets a refresh token cookie.
//
// This endpoint supports two authentication methods:
// 1. Password-based authentication (username + password)
// 2. SSO authentication (OAuth2 authorization code)
// 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) {
// Authentication: Not required (public endpoint).
// Returns: User info, access token, and token expiry.
func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) (*v1pb.SignInResponse, error) {
var existingUser *store.User
// Authentication Method 1: Password-based authentication
@ -197,99 +177,162 @@ func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSe
return nil, status.Errorf(codes.PermissionDenied, "user has been archived with username %s", existingUser.Username)
}
// Default session expiration time is 100 year
expireTime := time.Now().Add(100 * 365 * 24 * time.Hour)
if err := s.doSignIn(ctx, existingUser, expireTime); err != nil {
return nil, status.Errorf(codes.Internal, "failed to sign in, error: %v", err)
accessToken, accessExpiresAt, err := s.doSignIn(ctx, existingUser)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to sign in: %v", err)
}
return &v1pb.CreateSessionResponse{
User: convertUserFromStore(existingUser),
LastAccessedAt: timestamppb.Now(),
return &v1pb.SignInResponse{
User: convertUserFromStore(existingUser),
AccessToken: accessToken,
AccessTokenExpiresAt: timestamppb.New(accessExpiresAt),
}, 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 := auth.GenerateSessionID()
// Track session in user settings
if err := s.trackUserSession(ctx, user.ID, sessionID); err != nil {
// Log the error but don't fail the login if session tracking fails
// This ensures backward compatibility
slog.Error("failed to track user session", "error", err)
}
// Set session cookie for web use
sessionCookie, err := s.buildSessionCookie(ctx, sessionID, expireTime)
// 1. Generates refresh token and access token.
// 2. Stores refresh token metadata in user_setting.
// 3. Sets refresh token as HttpOnly cookie.
// 4. Returns access token and its expiry time.
func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User) (string, time.Time, error) {
// Generate refresh token
tokenID := util.GenUUID()
refreshToken, refreshExpiresAt, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(s.Secret))
if err != nil {
return status.Errorf(codes.Internal, "failed to build session cookie, error: %v", err)
}
if err := SetResponseHeader(ctx, "Set-Cookie", sessionCookie); err != nil {
return status.Errorf(codes.Internal, "failed to set response header, error: %v", err)
return "", time.Time{}, status.Errorf(codes.Internal, "failed to generate refresh token: %v", err)
}
return nil
// Store refresh token metadata
clientInfo := s.extractClientInfo(ctx)
refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(refreshExpiresAt),
CreatedAt: timestamppb.Now(),
ClientInfo: clientInfo,
}
if err := s.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord); err != nil {
slog.Error("failed to store refresh token", "error", err)
}
// Set refresh token cookie
refreshCookie := s.buildRefreshTokenCookie(ctx, refreshToken, refreshExpiresAt)
if err := SetResponseHeader(ctx, "Set-Cookie", refreshCookie); err != nil {
return "", time.Time{}, status.Errorf(codes.Internal, "failed to set refresh token cookie: %v", err)
}
// Generate access token
accessToken, accessExpiresAt, err := auth.GenerateAccessTokenV2(
user.ID,
user.Username,
string(user.Role),
string(user.RowStatus),
[]byte(s.Secret),
)
if err != nil {
return "", time.Time{}, status.Errorf(codes.Internal, "failed to generate access token: %v", err)
}
return accessToken, accessExpiresAt, nil
}
// DeleteSession terminates the current user session (logout).
// SignOut terminates the user's authentication.
// Revokes the refresh token and clears the authentication cookie.
//
// 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)
// Authentication: Required (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 {
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not found")
}
// Check if we have a session ID (from cookie-based auth)
if sessionID := auth.GetSessionID(ctx); sessionID != "" {
// Remove session from user settings
if err := s.Store.RemoveUserSession(ctx, user.ID, sessionID); err != nil {
slog.Error("failed to remove user session", "error", err)
func (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*emptypb.Empty, error) {
// Get user from access token claims
claims := auth.GetUserClaims(ctx)
if claims != nil {
// Revoke refresh token if we can identify it
refreshToken := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
if cookies := md.Get("cookie"); len(cookies) > 0 {
refreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0])
}
}
if refreshToken != "" {
refreshClaims, err := auth.ParseRefreshToken(refreshToken, []byte(s.Secret))
if err == nil {
// Remove refresh token from user_setting by token_id
_ = s.Store.RemoveUserRefreshToken(ctx, claims.UserID, refreshClaims.TokenID)
}
}
}
// Clear refresh token cookie
if err := s.clearAuthCookies(ctx); err != nil {
return nil, status.Errorf(codes.Internal, "failed to clear auth cookies, error: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) clearAuthCookies(ctx context.Context) error {
// Clear session cookie
sessionCookie, err := s.buildSessionCookie(ctx, "", time.Time{})
if err != nil {
return errors.Wrap(err, "failed to build session cookie")
// RefreshToken exchanges a valid refresh token for a new access token.
//
// This endpoint:
// 1. Extracts the refresh token from the HttpOnly cookie (memos_refresh)
// 2. Validates the refresh token against the database (checking expiry and revocation)
// 3. Generates a new short-lived access token (15 minutes)
// 4. Returns the new access token and its expiry time
//
// The refresh token remains valid and is not rotated.
// Client should store the new access token in memory and use it for API requests.
//
// Authentication: Requires valid refresh token in cookie (public endpoint)
// Returns: New access token and expiry timestamp.
func (s *APIV1Service) RefreshToken(ctx context.Context, _ *v1pb.RefreshTokenRequest) (*v1pb.RefreshTokenResponse, error) {
// Extract refresh token from cookie
refreshToken := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
if cookies := md.Get("cookie"); len(cookies) > 0 {
refreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0])
}
}
// Set cookie in the response
if err := SetResponseHeader(ctx, "Set-Cookie", sessionCookie); err != nil {
return errors.Wrap(err, "failed to set response header")
if refreshToken == "" {
return nil, status.Errorf(codes.Unauthenticated, "refresh token not found")
}
// Validate refresh token
authenticator := auth.NewAuthenticator(s.Store, s.Secret)
user, _, err := authenticator.AuthenticateByRefreshToken(ctx, refreshToken)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid refresh token: %v", err)
}
// Generate new access token
accessToken, expiresAt, err := auth.GenerateAccessTokenV2(
user.ID,
user.Username,
string(user.Role),
string(user.RowStatus),
[]byte(s.Secret),
)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err)
}
return &v1pb.RefreshTokenResponse{
AccessToken: accessToken,
ExpiresAt: timestamppb.New(expiresAt),
}, nil
}
func (s *APIV1Service) clearAuthCookies(ctx context.Context) error {
// Clear refresh token cookie
refreshCookie := s.buildRefreshTokenCookie(ctx, "", time.Time{})
if err := SetResponseHeader(ctx, "Set-Cookie", refreshCookie); err != nil {
return errors.Wrap(err, "failed to set refresh cookie")
}
return nil
}
func (*APIV1Service) buildSessionCookie(ctx context.Context, sessionCookieValue string, expireTime time.Time) (string, error) {
func (*APIV1Service) buildRefreshTokenCookie(ctx context.Context, refreshToken string, expireTime time.Time) string {
attrs := []string{
fmt.Sprintf("%s=%s", auth.SessionCookieName, sessionCookieValue),
fmt.Sprintf("%s=%s", auth.RefreshTokenCookieName, refreshToken),
"Path=/",
"HttpOnly",
}
@ -300,7 +343,7 @@ func (*APIV1Service) buildSessionCookie(ctx context.Context, sessionCookieValue
}
// Try to determine if the request is HTTPS by checking the origin header
// Default to non-HTTPS (Strict SameSite) if metadata is not available
// Default to non-HTTPS (Lax SameSite) if metadata is not available
isHTTPS := false
if md, ok := metadata.FromIncomingContext(ctx); ok {
for _, v := range md.Get("origin") {
@ -312,15 +355,14 @@ func (*APIV1Service) buildSessionCookie(ctx context.Context, sessionCookieValue
}
if isHTTPS {
attrs = append(attrs, "SameSite=None")
attrs = append(attrs, "Secure")
attrs = append(attrs, "SameSite=Lax", "Secure")
} else {
attrs = append(attrs, "SameSite=Strict")
attrs = append(attrs, "SameSite=Lax")
}
return strings.Join(attrs, "; "), nil
return strings.Join(attrs, "; ")
}
func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error) {
func (s *APIV1Service) fetchCurrentUser(ctx context.Context) (*store.User, error) {
userID := auth.GetUserID(ctx)
if userID == 0 {
return nil, nil
@ -337,27 +379,6 @@ func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error)
return user, nil
}
// 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)
session := &storepb.SessionsUserSetting_Session{
SessionId: sessionID,
CreateTime: timestamppb.Now(),
LastAccessedTime: timestamppb.Now(),
ClientInfo: clientInfo,
}
return s.Store.AddUserSession(ctx, userID, session)
}
// extractClientInfo extracts comprehensive client information from the request context.
//
// This function parses metadata from the gRPC context to extract:

View File

@ -43,6 +43,10 @@ func (*MetadataInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc
if xri := header.Get("X-Real-Ip"); xri != "" {
md.Set("x-real-ip", xri)
}
// Forward Cookie header for authentication methods that need it (e.g., RefreshToken)
if cookie := header.Get("Cookie"); cookie != "" {
md.Set("cookie", cookie)
}
// Set metadata in context so services can use metadata.FromIncomingContext()
ctx = metadata.NewIncomingContext(ctx, md)
@ -198,9 +202,16 @@ func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("authentication required"))
}
// Set user in context (may be nil for public endpoints)
// Set context based on auth result
if result != nil {
ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken)
if result.Claims != nil {
// Access Token V2 - stateless, use claims
ctx = auth.SetUserClaimsInContext(ctx, result.Claims)
ctx = context.WithValue(ctx, auth.UserIDContextKey, result.Claims.UserID)
} else if result.User != nil {
// PAT or legacy auth - have full user
ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken)
}
}
return next(ctx, req)

View File

@ -45,21 +45,27 @@ func (s *ConnectServiceHandler) UpdateInstanceSetting(ctx context.Context, req *
// We use connectWithHeaderCarrier helper to inject a header carrier into the context,
// which allows the service to set headers in a protocol-agnostic way.
func (s *ConnectServiceHandler) GetCurrentSession(ctx context.Context, req *connect.Request[v1pb.GetCurrentSessionRequest]) (*connect.Response[v1pb.GetCurrentSessionResponse], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.GetCurrentSessionResponse, error) {
return s.APIV1Service.GetCurrentSession(ctx, req.Msg)
func (s *ConnectServiceHandler) GetCurrentUser(ctx context.Context, req *connect.Request[v1pb.GetCurrentUserRequest]) (*connect.Response[v1pb.GetCurrentUserResponse], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.GetCurrentUserResponse, error) {
return s.APIV1Service.GetCurrentUser(ctx, req.Msg)
})
}
func (s *ConnectServiceHandler) CreateSession(ctx context.Context, req *connect.Request[v1pb.CreateSessionRequest]) (*connect.Response[v1pb.CreateSessionResponse], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.CreateSessionResponse, error) {
return s.APIV1Service.CreateSession(ctx, req.Msg)
func (s *ConnectServiceHandler) SignIn(ctx context.Context, req *connect.Request[v1pb.SignInRequest]) (*connect.Response[v1pb.SignInResponse], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.SignInResponse, error) {
return s.APIV1Service.SignIn(ctx, req.Msg)
})
}
func (s *ConnectServiceHandler) DeleteSession(ctx context.Context, req *connect.Request[v1pb.DeleteSessionRequest]) (*connect.Response[emptypb.Empty], error) {
func (s *ConnectServiceHandler) SignOut(ctx context.Context, req *connect.Request[v1pb.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*emptypb.Empty, error) {
return s.APIV1Service.DeleteSession(ctx, req.Msg)
return s.APIV1Service.SignOut(ctx, req.Msg)
})
}
func (s *ConnectServiceHandler) RefreshToken(ctx context.Context, req *connect.Request[v1pb.RefreshTokenRequest]) (*connect.Response[v1pb.RefreshTokenResponse], error) {
return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.RefreshTokenResponse, error) {
return s.APIV1Service.RefreshToken(ctx, req.Msg)
})
}
@ -145,40 +151,40 @@ func (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *conne
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) ListUserAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListUserAccessTokensRequest]) (*connect.Response[v1pb.ListUserAccessTokensResponse], error) {
resp, err := s.APIV1Service.ListUserAccessTokens(ctx, req.Msg)
func (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) {
resp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) CreateUserAccessToken(ctx context.Context, req *connect.Request[v1pb.CreateUserAccessTokenRequest]) (*connect.Response[v1pb.UserAccessToken], error) {
resp, err := s.APIV1Service.CreateUserAccessToken(ctx, req.Msg)
func (s *ConnectServiceHandler) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.CreatePersonalAccessTokenRequest]) (*connect.Response[v1pb.CreatePersonalAccessTokenResponse], error) {
resp, err := s.APIV1Service.CreatePersonalAccessToken(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) DeleteUserAccessToken(ctx context.Context, req *connect.Request[v1pb.DeleteUserAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeleteUserAccessToken(ctx, req.Msg)
func (s *ConnectServiceHandler) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeletePersonalAccessToken(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) ListUserSessions(ctx context.Context, req *connect.Request[v1pb.ListUserSessionsRequest]) (*connect.Response[v1pb.ListUserSessionsResponse], error) {
resp, err := s.APIV1Service.ListUserSessions(ctx, req.Msg)
func (s *ConnectServiceHandler) ListSessions(ctx context.Context, req *connect.Request[v1pb.ListSessionsRequest]) (*connect.Response[v1pb.ListSessionsResponse], error) {
resp, err := s.APIV1Service.ListSessions(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) RevokeUserSession(ctx context.Context, req *connect.Request[v1pb.RevokeUserSessionRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.RevokeUserSession(ctx, req.Msg)
func (s *ConnectServiceHandler) RevokeSession(ctx context.Context, req *connect.Request[v1pb.RevokeSessionRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.RevokeSession(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}

View File

@ -14,7 +14,7 @@ import (
)
func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb.CreateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -41,7 +41,7 @@ func (s *APIV1Service) ListIdentityProviders(ctx context.Context, _ *v1pb.ListId
// Default to lowest-privilege role, update later based on real role
currentUserRole := store.RoleUser
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err == nil && currentUser != nil {
currentUserRole = currentUser.Role
}
@ -70,7 +70,7 @@ func (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.Ge
// Default to lowest-privilege role, update later based on real role
currentUserRole := store.RoleUser
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err == nil && currentUser != nil {
currentUserRole = currentUser.Role
}
@ -80,7 +80,7 @@ func (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.Ge
}
func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb.UpdateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -121,7 +121,7 @@ func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb
}
func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb.DeleteIdentityProviderRequest) (*emptypb.Empty, error) {
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}

View File

@ -66,7 +66,7 @@ func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.Get
// For storage setting, only host can get it.
if instanceSetting.Key == storepb.InstanceSettingKey_STORAGE {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -79,7 +79,7 @@ func (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.Get
}
func (s *APIV1Service) UpdateInstanceSetting(ctx context.Context, request *v1pb.UpdateInstanceSettingRequest) (*v1pb.InstanceSetting, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -14,7 +14,7 @@ import (
)
func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.SetMemoAttachmentsRequest) (*emptypb.Empty, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -14,7 +14,7 @@ import (
)
func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMemoRelationsRequest) (*emptypb.Empty, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -84,7 +84,7 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}

View File

@ -22,7 +22,7 @@ import (
)
func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
@ -149,7 +149,7 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
memoFind.Filters = append(memoFind.Filters, request.Filter)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
@ -276,7 +276,7 @@ func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest
return nil, status.Errorf(codes.NotFound, "memo not found")
}
if memo.Visibility != store.Public {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
@ -326,7 +326,7 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
return nil, status.Errorf(codes.NotFound, "memo not found")
}
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
@ -464,7 +464,7 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
return nil, status.Errorf(codes.NotFound, "memo not found")
}
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
@ -615,7 +615,7 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}

View File

@ -33,7 +33,7 @@ func (s *APIV1Service) ListMemoReactions(ctx context.Context, request *v1pb.List
}
func (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.UpsertMemoReactionRequest) (*v1pb.Reaction, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
@ -55,7 +55,7 @@ func (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.Ups
}
func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.DeleteMemoReactionRequest) (*emptypb.Empty, error) {
user, err := s.GetCurrentUser(ctx)
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -49,7 +49,7 @@ func (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShor
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -91,7 +91,7 @@ func (s *APIV1Service) GetShortcut(ctx context.Context, request *v1pb.GetShortcu
return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -130,7 +130,7 @@ func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateS
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -202,7 +202,7 @@ func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateS
return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -274,7 +274,7 @@ func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteS
return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -0,0 +1,655 @@
package test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/internal/util"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/auth"
"github.com/usememos/memos/store"
)
func TestAuthenticatorAccessTokenV2(t *testing.T) {
ctx := context.Background()
t.Run("authenticates valid access token v2", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
// Create a test user
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate access token v2
token, _, err := auth.GenerateAccessTokenV2(
user.ID,
user.Username,
string(user.Role),
string(user.RowStatus),
[]byte(ts.Secret),
)
require.NoError(t, err)
// Authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
claims, err := authenticator.AuthenticateByAccessTokenV2(token)
require.NoError(t, err)
assert.NotNil(t, claims)
assert.Equal(t, user.ID, claims.UserID)
assert.Equal(t, user.Username, claims.Username)
assert.Equal(t, string(user.Role), claims.Role)
assert.Equal(t, string(user.RowStatus), claims.Status)
})
t.Run("fails with invalid token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, err := authenticator.AuthenticateByAccessTokenV2("invalid-token")
assert.Error(t, err)
})
t.Run("fails with wrong secret", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate token with one secret
token, _, err := auth.GenerateAccessTokenV2(
user.ID,
user.Username,
string(user.Role),
string(user.RowStatus),
[]byte("secret-1"),
)
require.NoError(t, err)
// Try to authenticate with different secret
authenticator := auth.NewAuthenticator(ts.Store, "secret-2")
_, err = authenticator.AuthenticateByAccessTokenV2(token)
assert.Error(t, err)
})
}
func TestAuthenticatorRefreshToken(t *testing.T) {
ctx := context.Background()
t.Run("authenticates valid refresh token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
// Create a test user
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Create refresh token record in store
tokenID := util.GenUUID()
refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord)
require.NoError(t, err)
// Generate refresh token JWT
token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))
require.NoError(t, err)
// Authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
authenticatedUser, returnedTokenID, err := authenticator.AuthenticateByRefreshToken(ctx, token)
require.NoError(t, err)
assert.NotNil(t, authenticatedUser)
assert.Equal(t, user.ID, authenticatedUser.ID)
assert.Equal(t, tokenID, returnedTokenID)
})
t.Run("fails with revoked token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
tokenID := util.GenUUID()
// Generate refresh token JWT but don't store it in database (simulates revocation)
token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))
require.NoError(t, err)
// Try to authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)
assert.Error(t, err)
assert.Contains(t, err.Error(), "revoked")
})
t.Run("fails with expired token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Create expired refresh token record in store
tokenID := util.GenUUID()
expiredToken := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, expiredToken)
require.NoError(t, err)
// Generate refresh token JWT (JWT itself isn't expired yet)
token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))
require.NoError(t, err)
// Try to authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
})
t.Run("fails with archived user", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Create valid refresh token
tokenID := util.GenUUID()
refreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord)
require.NoError(t, err)
token, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))
require.NoError(t, err)
// Archive the user
archivedStatus := store.Archived
_, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{
ID: user.ID,
RowStatus: &archivedStatus,
})
require.NoError(t, err)
// Try to authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)
assert.Error(t, err)
assert.Contains(t, err.Error(), "archived")
})
}
func TestAuthenticatorPAT(t *testing.T) {
ctx := context.Background()
t.Run("authenticates valid PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
// Create a test user
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate PAT
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
// Store PAT in database
patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)
require.NoError(t, err)
// Authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
authenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token)
require.NoError(t, err)
assert.NotNil(t, authenticatedUser)
assert.NotNil(t, pat)
assert.Equal(t, user.ID, authenticatedUser.ID)
assert.Equal(t, tokenID, pat.TokenId)
})
t.Run("fails with invalid PAT format", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err := authenticator.AuthenticateByPAT(ctx, "invalid-token-without-prefix")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid PAT format")
})
t.Run("fails with non-existent PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
// Generate a PAT but don't store it
token := auth.GeneratePersonalAccessToken()
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err := authenticator.AuthenticateByPAT(ctx, token)
assert.Error(t, err)
})
t.Run("fails with expired PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate and store expired PAT
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
expiredPAT := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Expired PAT",
ExpiresAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, expiredPAT)
require.NoError(t, err)
// Try to authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err = authenticator.AuthenticateByPAT(ctx, token)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
})
t.Run("succeeds with non-expiring PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate and store PAT without expiration
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Never-expiring PAT",
ExpiresAt: nil, // No expiration
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)
require.NoError(t, err)
// Authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
authenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token)
require.NoError(t, err)
assert.NotNil(t, authenticatedUser)
assert.NotNil(t, pat)
assert.Nil(t, pat.ExpiresAt)
})
t.Run("fails with archived user", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Generate and store PAT
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)
require.NoError(t, err)
// Archive the user
archivedStatus := store.Archived
_, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{
ID: user.ID,
RowStatus: &archivedStatus,
})
require.NoError(t, err)
// Try to authenticate
authenticator := auth.NewAuthenticator(ts.Store, ts.Secret)
_, _, err = authenticator.AuthenticateByPAT(ctx, token)
assert.Error(t, err)
assert.Contains(t, err.Error(), "archived")
})
}
func TestStoreRefreshTokenMethods(t *testing.T) {
ctx := context.Background()
t.Run("adds and retrieves refresh token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
tokenID := util.GenUUID()
token := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, token)
require.NoError(t, err)
// Retrieve tokens
tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, tokens, 1)
assert.Equal(t, tokenID, tokens[0].TokenId)
})
t.Run("retrieves specific refresh token by ID", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
tokenID := util.GenUUID()
token := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, token)
require.NoError(t, err)
// Retrieve specific token
retrievedToken, err := ts.Store.GetUserRefreshTokenByID(ctx, user.ID, tokenID)
require.NoError(t, err)
assert.NotNil(t, retrievedToken)
assert.Equal(t, tokenID, retrievedToken.TokenId)
})
t.Run("removes refresh token", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
tokenID := util.GenUUID()
token := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID,
ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, token)
require.NoError(t, err)
// Remove token
err = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID)
require.NoError(t, err)
// Verify removal
tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, tokens, 0)
})
t.Run("handles multiple refresh tokens", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Add multiple tokens
tokenID1 := util.GenUUID()
tokenID2 := util.GenUUID()
token1 := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID1,
ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),
CreatedAt: timestamppb.Now(),
}
token2 := &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: tokenID2,
ExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserRefreshToken(ctx, user.ID, token1)
require.NoError(t, err)
err = ts.Store.AddUserRefreshToken(ctx, user.ID, token2)
require.NoError(t, err)
// Retrieve all tokens
tokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, tokens, 2)
// Remove one token
err = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID1)
require.NoError(t, err)
// Verify only one token remains
tokens, err = ts.Store.GetUserRefreshTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, tokens, 1)
assert.Equal(t, tokenID2, tokens[0].TokenId)
})
}
func TestStorePersonalAccessTokenMethods(t *testing.T) {
ctx := context.Background()
t.Run("adds and retrieves PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Retrieve PATs
pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, pats, 1)
assert.Equal(t, tokenID, pats[0].TokenId)
assert.Equal(t, tokenHash, pats[0].TokenHash)
})
t.Run("removes PAT", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Remove PAT
err = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID)
require.NoError(t, err)
// Verify removal
pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, pats, 0)
})
t.Run("updates PAT last used time", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Update last used time
lastUsed := timestamppb.Now()
err = ts.Store.UpdatePATLastUsed(ctx, user.ID, tokenID, lastUsed)
require.NoError(t, err)
// Verify update
pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, pats, 1)
assert.NotNil(t, pats[0].LastUsedAt)
})
t.Run("handles multiple PATs", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
// Add multiple PATs
token1 := auth.GeneratePersonalAccessToken()
tokenHash1 := auth.HashPersonalAccessToken(token1)
tokenID1 := util.GenUUID()
token2 := auth.GeneratePersonalAccessToken()
tokenHash2 := auth.HashPersonalAccessToken(token2)
tokenID2 := util.GenUUID()
pat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID1,
TokenHash: tokenHash1,
Description: "PAT 1",
CreatedAt: timestamppb.Now(),
}
pat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID2,
TokenHash: tokenHash2,
Description: "PAT 2",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat1)
require.NoError(t, err)
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat2)
require.NoError(t, err)
// Retrieve all PATs
pats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, pats, 2)
// Remove one PAT
err = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID1)
require.NoError(t, err)
// Verify only one PAT remains
pats, err = ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)
require.NoError(t, err)
assert.Len(t, pats, 1)
assert.Equal(t, tokenID2, pats[0].TokenId)
})
t.Run("finds user by PAT hash", func(t *testing.T) {
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "testuser")
require.NoError(t, err)
token := auth.GeneratePersonalAccessToken()
tokenHash := auth.HashPersonalAccessToken(token)
tokenID := util.GenUUID()
pat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: "Test PAT",
CreatedAt: timestamppb.Now(),
}
err = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)
require.NoError(t, err)
// Find user by PAT hash
result, err := ts.Store.GetUserByPATHash(ctx, tokenHash)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, user.ID, result.UserID)
assert.NotNil(t, result.User)
assert.Equal(t, user.Username, result.User.Username)
assert.NotNil(t, result.PAT)
assert.Equal(t, tokenID, result.PAT.TokenId)
})
}

View File

@ -7,12 +7,10 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/ast"
"github.com/labstack/echo/v4"
@ -32,7 +30,7 @@ import (
)
func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -107,7 +105,7 @@ func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest
func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
// Get current user (might be nil for unauthenticated requests)
currentUser, _ := s.GetCurrentUser(ctx)
currentUser, _ := s.fetchCurrentUser(ctx)
// Check if there are any existing users (for first-time setup detection)
limitOne := 1
@ -190,7 +188,7 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -278,7 +276,7 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -318,7 +316,7 @@ func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUser
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -355,7 +353,7 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -444,7 +442,7 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
return nil, status.Errorf(codes.InvalidArgument, "invalid parent name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -498,7 +496,7 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
return response, nil
}
// ListUserAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
// ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
//
// Personal Access Tokens are used for:
// - Mobile app authentication
@ -508,75 +506,46 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
//
// Security:
// - Only the token owner can list their tokens
// - Returns full token strings (so users can manage/revoke them)
// - Returns token metadata only (not the actual token value)
// - 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) {
func (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1pb.ListPersonalAccessTokensRequest) (*v1pb.ListPersonalAccessTokensResponse, error) {
userID, err := ExtractUserIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
// Verify permission
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
tokens, err := s.Store.GetUserPersonalAccessTokens(ctx, userID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
return nil, status.Errorf(codes.Internal, "failed to get access tokens: %v", err)
}
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
}
accessTokens := []*v1pb.UserAccessToken{}
for _, userAccessToken := range userAccessTokens {
claims := &auth.ClaimsMessage{}
_, err := jwt.ParseWithClaims(userAccessToken.AccessToken, claims, func(t *jwt.Token) (any, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
}
if kid, ok := t.Header["kid"].(string); ok {
if kid == "v1" {
return []byte(s.Secret), nil
}
}
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
})
if err != nil {
// If the access token is invalid or expired, just ignore it.
continue
personalAccessTokens := make([]*v1pb.PersonalAccessToken, len(tokens))
for i, token := range tokens {
personalAccessTokens[i] = &v1pb.PersonalAccessToken{
Name: fmt.Sprintf("%s/personalAccessTokens/%s", request.Parent, token.TokenId),
Description: token.Description,
ExpiresAt: token.ExpiresAt,
CreatedAt: token.CreatedAt,
LastUsedAt: token.LastUsedAt,
}
accessTokenResponse := &v1pb.UserAccessToken{
Name: fmt.Sprintf("users/%d/accessTokens/%s", userID, userAccessToken.AccessToken),
AccessToken: userAccessToken.AccessToken,
Description: userAccessToken.Description,
IssuedAt: timestamppb.New(claims.IssuedAt.Time),
}
if claims.ExpiresAt != nil {
accessTokenResponse.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time)
}
accessTokens = append(accessTokens, accessTokenResponse)
}
// Sort by issued time in descending order.
slices.SortFunc(accessTokens, func(i, j *v1pb.UserAccessToken) int {
return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds)
})
response := &v1pb.ListUserAccessTokensResponse{
AccessTokens: accessTokens,
}
return response, nil
return &v1pb.ListPersonalAccessTokensResponse{PersonalAccessTokens: personalAccessTokens}, nil
}
// CreateUserAccessToken creates a new Personal Access Token (PAT) for a user.
// CreatePersonalAccessToken creates a new Personal Access Token (PAT) for a user.
//
// Use cases:
// - User manually creates token in settings for mobile app
@ -584,8 +553,8 @@ func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.L
// - User creates token for third-party integration
//
// Token properties:
// - JWT format signed with server secret
// - Contains user ID and username in claims
// - Random string with memos_pat_ prefix
// - SHA-256 hash stored in database
// - Optional expiration time (can be never-expiring)
// - User-provided description for identification
//
@ -596,66 +565,55 @@ func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.L
//
// 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) {
func (s *APIV1Service) CreatePersonalAccessToken(ctx context.Context, request *v1pb.CreatePersonalAccessTokenRequest) (*v1pb.CreatePersonalAccessTokenResponse, error) {
userID, err := ExtractUserIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
expiresAt := time.Time{}
if request.AccessToken.ExpiresAt != nil {
expiresAt = request.AccessToken.ExpiresAt.AsTime()
}
accessToken, err := auth.GenerateAccessToken(currentUser.Username, currentUser.ID, expiresAt, []byte(s.Secret))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err)
}
claims := &auth.ClaimsMessage{}
_, err = jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
// Verify permission
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if kid, ok := t.Header["kid"].(string); ok {
if kid == "v1" {
return []byte(s.Secret), nil
}
}
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse access token: %v", err)
}
// Upsert the access token to user setting store.
if err := s.UpsertAccessTokenToStore(ctx, currentUser, accessToken, request.AccessToken.Description); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert access token to store: %v", 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)))
}
userAccessToken := &v1pb.UserAccessToken{
Name: fmt.Sprintf("users/%d/accessTokens/%s", userID, accessToken),
AccessToken: accessToken,
Description: request.AccessToken.Description,
IssuedAt: timestamppb.New(claims.IssuedAt.Time),
patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
TokenId: tokenID,
TokenHash: tokenHash,
Description: request.Description,
ExpiresAt: expiresAt,
CreatedAt: timestamppb.Now(),
}
if claims.ExpiresAt != nil {
userAccessToken.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time)
if err := s.Store.AddUserPersonalAccessToken(ctx, userID, patRecord); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err)
}
return userAccessToken, nil
return &v1pb.CreatePersonalAccessTokenResponse{
PersonalAccessToken: &v1pb.PersonalAccessToken{
Name: fmt.Sprintf("%s/personalAccessTokens/%s", request.Parent, tokenID),
Description: request.Description,
ExpiresAt: expiresAt,
CreatedAt: patRecord.CreatedAt,
},
Token: token, // Only returned on creation
}, nil
}
// DeleteUserAccessToken revokes a Personal Access Token.
// DeletePersonalAccessToken revokes a Personal Access Token.
//
// This endpoint:
// 1. Removes the token from the user's access tokens list
@ -668,220 +626,118 @@ func (s *APIV1Service) CreateUserAccessToken(ctx context.Context, request *v1pb.
//
// 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}
func (s *APIV1Service) DeletePersonalAccessToken(ctx context.Context, request *v1pb.DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {
// Parse name: users/{user_id}/personalAccessTokens/{token_id}
parts := strings.Split(request.Name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "accessTokens" {
return nil, status.Errorf(codes.InvalidArgument, "invalid access token name format: %s", request.Name)
if len(parts) != 4 || parts[0] != "users" || parts[2] != "personalAccessTokens" {
return nil, status.Errorf(codes.InvalidArgument, "invalid personal access token name")
}
userID, err := ExtractUserIDFromName(fmt.Sprintf("users/%s", parts[1]))
userID, err := util.ConvertStringToInt32(parts[1])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
return nil, status.Errorf(codes.InvalidArgument, "invalid user ID: %v", err)
}
accessTokenToDelete := parts[3]
tokenID := parts[3]
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, currentUser.ID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
}
updatedUserAccessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
for _, userAccessToken := range userAccessTokens {
if userAccessToken.AccessToken == accessTokenToDelete {
continue
// Verify permission
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
updatedUserAccessTokens = append(updatedUserAccessTokens, userAccessToken)
}
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: currentUser.ID,
Key: storepb.UserSetting_ACCESS_TOKENS,
Value: &storepb.UserSetting_AccessTokens{
AccessTokens: &storepb.AccessTokensUserSetting{
AccessTokens: updatedUserAccessTokens,
},
},
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", 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
}
// 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) {
// ListSessions returns all active login sessions for a user.
// Each session represents a device/browser where the user is logged in,
// backed by refresh tokens with sliding expiration.
func (s *APIV1Service) ListSessions(ctx context.Context, request *v1pb.ListSessionsRequest) (*v1pb.ListSessionsResponse, error) {
userID, err := ExtractUserIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
userSessions, err := s.Store.GetUserSessions(ctx, userID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list sessions: %v", err)
}
sessions := []*v1pb.UserSession{}
for _, userSession := range userSessions {
sessionResponse := &v1pb.UserSession{
Name: fmt.Sprintf("users/%d/sessions/%s", userID, userSession.SessionId),
SessionId: userSession.SessionId,
CreateTime: userSession.CreateTime,
LastAccessedTime: userSession.LastAccessedTime,
// Verify permission.
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
if userSession.ClientInfo != nil {
sessionResponse.ClientInfo = &v1pb.UserSession_ClientInfo{
UserAgent: userSession.ClientInfo.UserAgent,
IpAddress: userSession.ClientInfo.IpAddress,
DeviceType: userSession.ClientInfo.DeviceType,
Os: userSession.ClientInfo.Os,
Browser: userSession.ClientInfo.Browser,
refreshTokens, err := s.Store.GetUserRefreshTokens(ctx, userID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get sessions: %v", err)
}
sessions := make([]*v1pb.Session, 0, len(refreshTokens))
for _, token := range refreshTokens {
session := &v1pb.Session{
Name: fmt.Sprintf("%s/sessions/%s", request.Parent, token.TokenId),
SessionId: token.TokenId,
CreateTime: token.CreatedAt,
}
if token.ClientInfo != nil {
session.ClientInfo = &v1pb.Session_ClientInfo{
UserAgent: token.ClientInfo.UserAgent,
IpAddress: token.ClientInfo.IpAddress,
DeviceType: token.ClientInfo.DeviceType,
Os: token.ClientInfo.Os,
Browser: token.ClientInfo.Browser,
}
}
sessions = append(sessions, sessionResponse)
sessions = append(sessions, session)
}
// Sort by last accessed time in descending order.
slices.SortFunc(sessions, func(i, j *v1pb.UserSession) int {
return int(j.LastAccessedTime.Seconds - i.LastAccessedTime.Seconds)
})
response := &v1pb.ListUserSessionsResponse{
Sessions: sessions,
}
return response, nil
return &v1pb.ListSessionsResponse{Sessions: sessions}, 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}
// RevokeSession revokes a specific login session.
// This invalidates the refresh token, forcing re-authentication on that device.
func (s *APIV1Service) RevokeSession(ctx context.Context, request *v1pb.RevokeSessionRequest) (*emptypb.Empty, error) {
// Parse name: users/{user_id}/sessions/{session_id}
parts := strings.Split(request.Name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "sessions" {
return nil, status.Errorf(codes.InvalidArgument, "invalid session name format: %s", request.Name)
return nil, status.Errorf(codes.InvalidArgument, "invalid session name")
}
userID, err := ExtractUserIDFromName(fmt.Sprintf("users/%s", parts[1]))
userID, err := util.ConvertStringToInt32(parts[1])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
return nil, status.Errorf(codes.InvalidArgument, "invalid user ID: %v", err)
}
sessionIDToRevoke := parts[3]
sessionID := parts[3]
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != userID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
// Verify permission.
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
if err := s.Store.RemoveUserSession(ctx, userID, sessionIDToRevoke); err != nil {
if err := s.Store.RemoveUserRefreshToken(ctx, userID, sessionID); err != nil {
return nil, status.Errorf(codes.Internal, "failed to revoke session: %v", err)
}
return &emptypb.Empty{}, nil
}
// UpsertUserSession adds or updates a user session.
func (s *APIV1Service) UpsertUserSession(ctx context.Context, userID int32, sessionID string, clientInfo *storepb.SessionsUserSetting_ClientInfo) error {
session := &storepb.SessionsUserSetting_Session{
SessionId: sessionID,
CreateTime: timestamppb.Now(),
LastAccessedTime: timestamppb.Now(),
ClientInfo: clientInfo,
}
return s.Store.AddUserSession(ctx, userID, session)
}
func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error {
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
if err != nil {
return errors.Wrap(err, "failed to get user access tokens")
}
userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
AccessToken: accessToken,
Description: description,
}
userAccessTokens = append(userAccessTokens, &userAccessToken)
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSetting_ACCESS_TOKENS,
Value: &storepb.UserSetting_AccessTokens{
AccessTokens: &storepb.AccessTokensUserSetting{
AccessTokens: userAccessTokens,
},
},
}); err != nil {
return errors.Wrap(err, "failed to upsert user setting")
}
return nil
}
func (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListUserWebhooksRequest) (*v1pb.ListUserWebhooksResponse, error) {
userID, err := ExtractUserIDFromName(request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -913,7 +769,7 @@ func (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.Crea
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -953,7 +809,7 @@ func (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.Upda
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -1025,7 +881,7 @@ func (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.Dele
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -1187,10 +1043,6 @@ func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {
switch key {
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]:
return storepb.UserSetting_GENERAL, nil
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_SESSIONS)]:
return storepb.UserSetting_SESSIONS, nil
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_ACCESS_TOKENS)]:
return storepb.UserSetting_ACCESS_TOKENS, nil
case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]:
return storepb.UserSetting_WEBHOOKS, nil
default:
@ -1203,10 +1055,6 @@ func convertSettingKeyFromStore(key storepb.UserSetting_Key) string {
switch key {
case storepb.UserSetting_GENERAL:
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]
case storepb.UserSetting_SESSIONS:
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_SESSIONS)]
case storepb.UserSetting_ACCESS_TOKENS:
return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_ACCESS_TOKENS)]
case storepb.UserSetting_SHORTCUTS:
return "SHORTCUTS" // Not defined in API proto
case storepb.UserSetting_WEBHOOKS:
@ -1226,18 +1074,6 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32
}
switch key {
case storepb.UserSetting_SESSIONS:
setting.Value = &v1pb.UserSetting_SessionsSetting_{
SessionsSetting: &v1pb.UserSetting_SessionsSetting{
Sessions: []*v1pb.UserSession{},
},
}
case storepb.UserSetting_ACCESS_TOKENS:
setting.Value = &v1pb.UserSetting_AccessTokensSetting_{
AccessTokensSetting: &v1pb.UserSetting_AccessTokensSetting{
AccessTokens: []*v1pb.UserAccessToken{},
},
}
case storepb.UserSetting_WEBHOOKS:
setting.Value = &v1pb.UserSetting_WebhooksSetting_{
WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{
@ -1273,46 +1109,6 @@ func convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32
GeneralSetting: getDefaultUserGeneralSetting(),
}
}
case storepb.UserSetting_SESSIONS:
sessions := storeSetting.GetSessions()
apiSessions := make([]*v1pb.UserSession, 0, len(sessions.Sessions))
for _, session := range sessions.Sessions {
apiSession := &v1pb.UserSession{
Name: fmt.Sprintf("users/%d/sessions/%s", userID, session.SessionId),
SessionId: session.SessionId,
CreateTime: session.CreateTime,
LastAccessedTime: session.LastAccessedTime,
ClientInfo: &v1pb.UserSession_ClientInfo{
UserAgent: session.ClientInfo.UserAgent,
IpAddress: session.ClientInfo.IpAddress,
DeviceType: session.ClientInfo.DeviceType,
Os: session.ClientInfo.Os,
Browser: session.ClientInfo.Browser,
},
}
apiSessions = append(apiSessions, apiSession)
}
setting.Value = &v1pb.UserSetting_SessionsSetting_{
SessionsSetting: &v1pb.UserSetting_SessionsSetting{
Sessions: apiSessions,
},
}
case storepb.UserSetting_ACCESS_TOKENS:
accessTokens := storeSetting.GetAccessTokens()
apiTokens := make([]*v1pb.UserAccessToken, 0, len(accessTokens.AccessTokens))
for _, token := range accessTokens.AccessTokens {
apiToken := &v1pb.UserAccessToken{
Name: fmt.Sprintf("users/%d/accessTokens/%s", userID, token.AccessToken),
AccessToken: token.AccessToken,
Description: token.Description,
}
apiTokens = append(apiTokens, apiToken)
}
setting.Value = &v1pb.UserSetting_AccessTokensSetting_{
AccessTokensSetting: &v1pb.UserSetting_AccessTokensSetting{
AccessTokens: apiTokens,
},
}
case storepb.UserSetting_WEBHOOKS:
webhooks := storeSetting.GetWebhooks()
apiWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks))
@ -1359,50 +1155,6 @@ func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key s
} else {
return nil, errors.Errorf("general setting is required")
}
case storepb.UserSetting_SESSIONS:
if sessions := apiSetting.GetSessionsSetting(); sessions != nil {
storeSessions := make([]*storepb.SessionsUserSetting_Session, 0, len(sessions.Sessions))
for _, session := range sessions.Sessions {
storeSession := &storepb.SessionsUserSetting_Session{
SessionId: session.SessionId,
CreateTime: session.CreateTime,
LastAccessedTime: session.LastAccessedTime,
ClientInfo: &storepb.SessionsUserSetting_ClientInfo{
UserAgent: session.ClientInfo.UserAgent,
IpAddress: session.ClientInfo.IpAddress,
DeviceType: session.ClientInfo.DeviceType,
Os: session.ClientInfo.Os,
Browser: session.ClientInfo.Browser,
},
}
storeSessions = append(storeSessions, storeSession)
}
storeSetting.Value = &storepb.UserSetting_Sessions{
Sessions: &storepb.SessionsUserSetting{
Sessions: storeSessions,
},
}
} else {
return nil, errors.Errorf("sessions setting is required")
}
case storepb.UserSetting_ACCESS_TOKENS:
if accessTokens := apiSetting.GetAccessTokensSetting(); accessTokens != nil {
storeTokens := make([]*storepb.AccessTokensUserSetting_AccessToken, 0, len(accessTokens.AccessTokens))
for _, token := range accessTokens.AccessTokens {
storeToken := &storepb.AccessTokensUserSetting_AccessToken{
AccessToken: token.AccessToken,
Description: token.Description,
}
storeTokens = append(storeTokens, storeToken)
}
storeSetting.Value = &storepb.UserSetting_AccessTokens{
AccessTokens: &storepb.AccessTokensUserSetting{
AccessTokens: storeTokens,
},
}
} else {
return nil, errors.Errorf("access tokens setting is required")
}
case storepb.UserSetting_WEBHOOKS:
if webhooks := apiSetting.GetWebhooksSetting(); webhooks != nil {
storeWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(webhooks.Webhooks))
@ -1542,7 +1294,7 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
}
// Verify the requesting user has permission to view these notifications
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -1588,7 +1340,7 @@ func (s *APIV1Service) UpdateUserNotification(ctx context.Context, request *v1pb
return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
@ -1653,7 +1405,7 @@ func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb
return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}

View File

@ -28,7 +28,7 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
RowStatus: &normalStatus,
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
@ -121,7 +121,7 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
currentUser, err := s.GetCurrentUser(ctx)
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}

View File

@ -76,9 +76,16 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
return
}
// Set user in context (may be nil for public endpoints)
// Set context based on auth result (may be nil for public endpoints)
if result != nil {
ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken)
if result.Claims != nil {
// Access Token V2 - stateless, use claims
ctx = auth.SetUserClaimsInContext(ctx, result.Claims)
ctx = context.WithValue(ctx, auth.UserIDContextKey, result.Claims.UserID)
} else if result.User != nil {
// PAT - have full user
ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken)
}
r = r.WithContext(ctx)
}

View File

@ -287,25 +287,32 @@ func (s *FileServerService) checkAttachmentPermission(ctx context.Context, c ech
}
// getCurrentUser retrieves the current authenticated user from the Echo context.
// It checks both session cookies and Bearer tokens for authentication.
// It checks Bearer tokens for authentication (Access Token V2 or PAT).
// Uses the shared Authenticator for consistent authentication logic.
func (s *FileServerService) getCurrentUser(ctx context.Context, c echo.Context) (*store.User, error) {
// Try session cookie authentication first
if cookie, err := c.Cookie(auth.SessionCookieName); err == nil && cookie.Value != "" {
user, err := s.authenticator.AuthenticateBySession(ctx, cookie.Value)
if err == nil && user != nil {
return user, nil
}
}
// Try JWT Bearer token authentication
// Try Bearer token authentication
authHeader := c.Request().Header.Get("Authorization")
if authHeader != "" {
parts := strings.Fields(authHeader)
if len(parts) == 2 && strings.EqualFold(parts[0], "bearer") {
user, err := s.authenticator.AuthenticateByJWT(ctx, parts[1])
if err == nil && user != nil {
return user, nil
token := auth.ExtractBearerToken(authHeader)
if token != "" {
// Try Access Token V2 (stateless)
if !strings.HasPrefix(token, auth.PersonalAccessTokenPrefix) {
claims, err := s.authenticator.AuthenticateByAccessTokenV2(token)
if err == nil && claims != nil {
// Get user from claims
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &claims.UserID})
if err == nil && user != nil {
return user, nil
}
}
}
// Try PAT
if strings.HasPrefix(token, auth.PersonalAccessTokenPrefix) {
user, _, err := s.authenticator.AuthenticateByPAT(ctx, token)
if err == nil && user != nil {
return user, nil
}
}
}
}

View File

@ -57,41 +57,37 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) GetUserSessionByID(ctx context.Context, sessionID string) (*store.UserSessionQueryResult, error) {
// Query user_setting that contains this sessionID in the sessions array
// Use JSON_SEARCH to check if sessionID exists in the array
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
SELECT
user_id,
value
FROM user_setting
WHERE ` + "`key`" + ` = 'SESSIONS'
AND JSON_SEARCH(value, 'one', ?, NULL, '$.sessions[*].sessionId') IS NOT NULL
WHERE ` + "`key`" + ` = 'PERSONAL_ACCESS_TOKENS'
AND JSON_SEARCH(value, 'one', ?, NULL, '$.tokens[*].tokenHash') IS NOT NULL
`
var userID int32
var sessionsJSON string
var tokensJSON string
err := d.db.QueryRowContext(ctx, query, sessionID).Scan(&userID, &sessionsJSON)
err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)
if err != nil {
return nil, err
}
// Parse the entire sessions list using protobuf unmarshaler
sessionsUserSetting := &storepb.SessionsUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(sessionsJSON), sessionsUserSetting); err != nil {
patsUserSetting := &storepb.PersonalAccessTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {
return nil, err
}
// Find the specific session by ID
for _, session := range sessionsUserSetting.Sessions {
if session.SessionId == sessionID {
return &store.UserSessionQueryResult{
UserID: userID,
Session: session,
for _, pat := range patsUserSetting.Tokens {
if pat.TokenHash == tokenHash {
return &store.PATQueryResult{
UserID: userID,
PAT: pat,
}, nil
}
}
return nil, errors.New("session not found")
return nil, errors.New("PAT not found")
}

View File

@ -70,45 +70,41 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) GetUserSessionByID(ctx context.Context, sessionID string) (*store.UserSessionQueryResult, error) {
// Query user_setting that contains this sessionID in the sessions array
// Use EXISTS with jsonb_array_elements to check array membership
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
SELECT
user_setting.user_id,
user_setting.value
FROM user_setting
WHERE user_setting.key = 'SESSIONS'
WHERE user_setting.key = 'PERSONAL_ACCESS_TOKENS'
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(user_setting.value::jsonb->'sessions') AS session
WHERE session->>'sessionId' = $1
SELECT 1
FROM jsonb_array_elements(user_setting.value::jsonb->'tokens') AS token
WHERE token->>'tokenHash' = $1
)
`
var userID int32
var sessionsJSON string
var tokensJSON string
err := d.db.QueryRowContext(ctx, query, sessionID).Scan(&userID, &sessionsJSON)
err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)
if err != nil {
return nil, err
}
// Parse the entire sessions list using protobuf unmarshaler
sessionsUserSetting := &storepb.SessionsUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(sessionsJSON), sessionsUserSetting); err != nil {
patsUserSetting := &storepb.PersonalAccessTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {
return nil, err
}
// Find the specific session by ID
for _, session := range sessionsUserSetting.Sessions {
if session.SessionId == sessionID {
return &store.UserSessionQueryResult{
UserID: userID,
Session: session,
for _, pat := range patsUserSetting.Tokens {
if pat.TokenHash == tokenHash {
return &store.PATQueryResult{
UserID: userID,
PAT: pat,
}, nil
}
}
return nil, errors.New("session not found")
return nil, errors.New("PAT not found")
}

View File

@ -69,45 +69,41 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil
}
func (d *DB) GetUserSessionByID(ctx context.Context, sessionID string) (*store.UserSessionQueryResult, error) {
// Query user_setting that contains this sessionID in the sessions array
// Use EXISTS with json_each to properly check array membership
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
SELECT
user_setting.user_id,
user_setting.value
FROM user_setting
WHERE user_setting.key = 'SESSIONS'
AND EXISTS (
SELECT 1
FROM json_each(json_extract(user_setting.value, '$.sessions')) AS session
WHERE json_extract(session.value, '$.sessionId') = ?
)
WHERE user_setting.key = 'PERSONAL_ACCESS_TOKENS'
AND EXISTS (
SELECT 1
FROM json_each(json_extract(user_setting.value, '$.tokens')) AS token
WHERE json_extract(token.value, '$.tokenHash') = ?
)
`
var userID int32
var sessionsJSON string
var tokensJSON string
err := d.db.QueryRowContext(ctx, query, sessionID).Scan(&userID, &sessionsJSON)
err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)
if err != nil {
return nil, err
}
// Parse the entire sessions list using protobuf unmarshaler
sessionsUserSetting := &storepb.SessionsUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(sessionsJSON), sessionsUserSetting); err != nil {
patsUserSetting := &storepb.PersonalAccessTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {
return nil, err
}
// Find the specific session by ID
for _, session := range sessionsUserSetting.Sessions {
if session.SessionId == sessionID {
return &store.UserSessionQueryResult{
UserID: userID,
Session: session,
for _, pat := range patsUserSetting.Tokens {
if pat.TokenHash == tokenHash {
return &store.PATQueryResult{
UserID: userID,
PAT: pat,
}, nil
}
}
return nil, errors.New("session not found")
return nil, errors.New("PAT not found")
}

View File

@ -48,7 +48,7 @@ type Driver interface {
// UserSetting model related methods.
UpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error)
ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error)
GetUserSessionByID(ctx context.Context, sessionID string) (*UserSessionQueryResult, error)
GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error)
// IdentityProvider model related methods.
CreateIdentityProvider(ctx context.Context, create *IdentityProvider) (*IdentityProvider, error)

View File

@ -21,10 +21,17 @@ type FindUserSetting struct {
Key storepb.UserSetting_Key
}
// UserSessionQueryResult contains the result of querying a single session by ID.
type UserSessionQueryResult struct {
UserID int32
Session *storepb.SessionsUserSetting_Session
// RefreshTokenQueryResult contains the result of querying a refresh token.
type RefreshTokenQueryResult struct {
UserID int32
RefreshToken *storepb.RefreshTokensUserSetting_RefreshToken
}
// PATQueryResult contains the result of querying a PAT by hash.
type PATQueryResult struct {
UserID int32
User *User
PAT *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken
}
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting) (*storepb.UserSetting, error) {
@ -95,164 +102,190 @@ func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*sto
return userSetting, nil
}
// GetUserAccessTokens returns the access tokens of the user.
func (s *Store) GetUserAccessTokens(ctx context.Context, userID int32) ([]*storepb.AccessTokensUserSetting_AccessToken, error) {
// GetUserByPATHash finds a user by PAT hash.
func (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) {
result, err := s.driver.GetUserByPATHash(ctx, tokenHash)
if err != nil {
return nil, err
}
// Fetch user info
user, err := s.GetUser(ctx, &FindUser{ID: &result.UserID})
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("user not found for PAT")
}
result.User = user
return result, nil
}
// GetUserRefreshTokens returns the refresh tokens of the user.
func (s *Store) GetUserRefreshTokens(ctx context.Context, userID int32) ([]*storepb.RefreshTokensUserSetting_RefreshToken, error) {
userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{
UserID: &userID,
Key: storepb.UserSetting_ACCESS_TOKENS,
Key: storepb.UserSetting_REFRESH_TOKENS,
})
if err != nil {
return nil, err
}
if userSetting == nil {
return []*storepb.AccessTokensUserSetting_AccessToken{}, nil
return []*storepb.RefreshTokensUserSetting_RefreshToken{}, nil
}
accessTokensUserSetting := userSetting.GetAccessTokens()
return accessTokensUserSetting.AccessTokens, nil
return userSetting.GetRefreshTokens().RefreshTokens, nil
}
// RemoveUserAccessToken remove the access token of the user.
func (s *Store) RemoveUserAccessToken(ctx context.Context, userID int32, token string) error {
oldAccessTokens, err := s.GetUserAccessTokens(ctx, userID)
// AddUserRefreshToken adds a new refresh token for the user.
func (s *Store) AddUserRefreshToken(ctx context.Context, userID int32, token *storepb.RefreshTokensUserSetting_RefreshToken) error {
tokens, err := s.GetUserRefreshTokens(ctx, userID)
if err != nil {
return err
}
newAccessTokens := make([]*storepb.AccessTokensUserSetting_AccessToken, 0, len(oldAccessTokens))
for _, t := range oldAccessTokens {
if token != t.AccessToken {
newAccessTokens = append(newAccessTokens, t)
tokens = append(tokens, token)
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_REFRESH_TOKENS,
Value: &storepb.UserSetting_RefreshTokens{
RefreshTokens: &storepb.RefreshTokensUserSetting{
RefreshTokens: tokens,
},
},
})
return err
}
// RemoveUserRefreshToken removes a refresh token from the user.
func (s *Store) RemoveUserRefreshToken(ctx context.Context, userID int32, tokenID string) error {
existingTokens, err := s.GetUserRefreshTokens(ctx, userID)
if err != nil {
return err
}
newTokens := make([]*storepb.RefreshTokensUserSetting_RefreshToken, 0, len(existingTokens))
for _, token := range existingTokens {
if token.TokenId != tokenID {
newTokens = append(newTokens, token)
}
}
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_ACCESS_TOKENS,
Value: &storepb.UserSetting_AccessTokens{
AccessTokens: &storepb.AccessTokensUserSetting{
AccessTokens: newAccessTokens,
Key: storepb.UserSetting_REFRESH_TOKENS,
Value: &storepb.UserSetting_RefreshTokens{
RefreshTokens: &storepb.RefreshTokensUserSetting{
RefreshTokens: newTokens,
},
},
})
return err
}
// GetUserSessions returns the sessions of the user.
func (s *Store) GetUserSessions(ctx context.Context, userID int32) ([]*storepb.SessionsUserSetting_Session, error) {
// GetUserRefreshTokenByID returns a specific refresh token.
func (s *Store) GetUserRefreshTokenByID(ctx context.Context, userID int32, tokenID string) (*storepb.RefreshTokensUserSetting_RefreshToken, error) {
tokens, err := s.GetUserRefreshTokens(ctx, userID)
if err != nil {
return nil, err
}
for _, token := range tokens {
if token.TokenId == tokenID {
return token, nil
}
}
return nil, nil
}
// GetUserPersonalAccessTokens returns the PATs of the user.
func (s *Store) GetUserPersonalAccessTokens(ctx context.Context, userID int32) ([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) {
userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{
UserID: &userID,
Key: storepb.UserSetting_SESSIONS,
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
})
if err != nil {
return nil, err
}
if userSetting == nil {
return []*storepb.SessionsUserSetting_Session{}, nil
return []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{}, nil
}
sessionsUserSetting := userSetting.GetSessions()
return sessionsUserSetting.Sessions, nil
return userSetting.GetPersonalAccessTokens().Tokens, nil
}
// RemoveUserSession removes the session of the user.
func (s *Store) RemoveUserSession(ctx context.Context, userID int32, sessionID string) error {
oldSessions, err := s.GetUserSessions(ctx, userID)
// AddUserPersonalAccessToken adds a new PAT for the user.
func (s *Store) AddUserPersonalAccessToken(ctx context.Context, userID int32, token *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken) error {
tokens, err := s.GetUserPersonalAccessTokens(ctx, userID)
if err != nil {
return err
}
newSessions := make([]*storepb.SessionsUserSetting_Session, 0, len(oldSessions))
for _, session := range oldSessions {
if sessionID != session.SessionId {
newSessions = append(newSessions, session)
tokens = append(tokens, token)
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
Value: &storepb.UserSetting_PersonalAccessTokens{
PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{
Tokens: tokens,
},
},
})
return err
}
// RemoveUserPersonalAccessToken removes a PAT from the user.
func (s *Store) RemoveUserPersonalAccessToken(ctx context.Context, userID int32, tokenID string) error {
existingTokens, err := s.GetUserPersonalAccessTokens(ctx, userID)
if err != nil {
return err
}
newTokens := make([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, 0, len(existingTokens))
for _, token := range existingTokens {
if token.TokenId != tokenID {
newTokens = append(newTokens, token)
}
}
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_SESSIONS,
Value: &storepb.UserSetting_Sessions{
Sessions: &storepb.SessionsUserSetting{
Sessions: newSessions,
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
Value: &storepb.UserSetting_PersonalAccessTokens{
PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{
Tokens: newTokens,
},
},
})
return err
}
// AddUserSession adds a new session for the user.
func (s *Store) AddUserSession(ctx context.Context, userID int32, session *storepb.SessionsUserSetting_Session) error {
existingSessions, err := s.GetUserSessions(ctx, userID)
// UpdatePATLastUsed updates the last_used_at timestamp of a PAT.
func (s *Store) UpdatePATLastUsed(ctx context.Context, userID int32, tokenID string, lastUsed *timestamppb.Timestamp) error {
tokens, err := s.GetUserPersonalAccessTokens(ctx, userID)
if err != nil {
return err
}
// Check if session already exists, update if it does
var updatedSessions []*storepb.SessionsUserSetting_Session
sessionExists := false
for _, existing := range existingSessions {
if existing.SessionId == session.SessionId {
updatedSessions = append(updatedSessions, session)
sessionExists = true
} else {
updatedSessions = append(updatedSessions, existing)
}
}
// If session doesn't exist, add it
if !sessionExists {
updatedSessions = append(updatedSessions, session)
}
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_SESSIONS,
Value: &storepb.UserSetting_Sessions{
Sessions: &storepb.SessionsUserSetting{
Sessions: updatedSessions,
},
},
})
return err
}
// UpdateUserSessionLastAccessed updates the last accessed time of a session.
func (s *Store) UpdateUserSessionLastAccessed(ctx context.Context, userID int32, sessionID string, lastAccessedTime *timestamppb.Timestamp) error {
sessions, err := s.GetUserSessions(ctx, userID)
if err != nil {
return err
}
for _, session := range sessions {
if session.SessionId == sessionID {
session.LastAccessedTime = lastAccessedTime
for _, token := range tokens {
if token.TokenId == tokenID {
token.LastUsedAt = lastUsed
break
}
}
_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{
UserId: userID,
Key: storepb.UserSetting_SESSIONS,
Value: &storepb.UserSetting_Sessions{
Sessions: &storepb.SessionsUserSetting{
Sessions: sessions,
Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS,
Value: &storepb.UserSetting_PersonalAccessTokens{
PersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{
Tokens: tokens,
},
},
})
return err
}
// GetUserSessionByID returns the session details for the given session ID.
// Uses database-specific JSON queries for efficient lookup without loading all sessions.
func (s *Store) GetUserSessionByID(ctx context.Context, sessionID string) (*UserSessionQueryResult, error) {
return s.driver.GetUserSessionByID(ctx, sessionID)
}
// GetUserWebhooks returns the webhooks of the user.
func (s *Store) GetUserWebhooks(ctx context.Context, userID int32) ([]*storepb.WebhooksUserSetting_Webhook, error) {
userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{
@ -392,6 +425,18 @@ func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {
return nil, err
}
userSetting.Value = &storepb.UserSetting_General{General: generalUserSetting}
case storepb.UserSetting_REFRESH_TOKENS:
refreshTokensUserSetting := &storepb.RefreshTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), refreshTokensUserSetting); err != nil {
return nil, err
}
userSetting.Value = &storepb.UserSetting_RefreshTokens{RefreshTokens: refreshTokensUserSetting}
case storepb.UserSetting_PERSONAL_ACCESS_TOKENS:
patsUserSetting := &storepb.PersonalAccessTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), patsUserSetting); err != nil {
return nil, err
}
userSetting.Value = &storepb.UserSetting_PersonalAccessTokens{PersonalAccessTokens: patsUserSetting}
case storepb.UserSetting_WEBHOOKS:
webhooksUserSetting := &storepb.WebhooksUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), webhooksUserSetting); err != nil {
@ -439,6 +484,20 @@ func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, er
return nil, err
}
raw.Value = string(value)
case storepb.UserSetting_REFRESH_TOKENS:
refreshTokensUserSetting := userSetting.GetRefreshTokens()
value, err := protojson.Marshal(refreshTokensUserSetting)
if err != nil {
return nil, err
}
raw.Value = string(value)
case storepb.UserSetting_PERSONAL_ACCESS_TOKENS:
patsUserSetting := userSetting.GetPersonalAccessTokens()
value, err := protojson.Marshal(patsUserSetting)
if err != nil {
return nil, err
}
raw.Value = string(value)
case storepb.UserSetting_WEBHOOKS:
webhooksUserSetting := userSetting.GetWebhooks()
value, err := protojson.Marshal(webhooksUserSetting)

21
web/src/auth-state.ts Normal file
View File

@ -0,0 +1,21 @@
// In-memory storage for access token (not persisted for security)
let accessToken: string | null = null;
let tokenExpiresAt: Date | null = null;
export const getAccessToken = (): string | null => accessToken;
export const setAccessToken = (token: string | null, expiresAt?: Date): void => {
accessToken = token;
tokenExpiresAt = expiresAt || null;
};
export const isTokenExpired = (): boolean => {
if (!tokenExpiresAt) return true;
// Consider expired 30 seconds before actual expiry for safety
return new Date() >= new Date(tokenExpiresAt.getTime() - 30000);
};
export const clearAccessToken = (): void => {
accessToken = null;
tokenExpiresAt = null;
};

View File

@ -1,4 +1,3 @@
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
import React, { useState } from "react";
import { toast } from "react-hot-toast";
import { Button } from "@/components/ui/button";
@ -9,13 +8,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { userServiceClient } from "@/connect";
import useCurrentUser from "@/hooks/useCurrentUser";
import useLoading from "@/hooks/useLoading";
import { UserAccessToken } from "@/types/proto/api/v1/user_service_pb";
import { CreatePersonalAccessTokenResponse } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: (created: UserAccessToken) => void;
onSuccess: (response: CreatePersonalAccessTokenResponse) => void;
}
interface State {
@ -28,18 +27,19 @@ function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) {
const currentUser = useCurrentUser();
const [state, setState] = useState({
description: "",
expiration: 3600 * 8,
expiration: 30, // Default: 30 days
});
const requestState = useLoading(false);
// Expiration options in days (0 = never expires)
const expirationOptions = [
{
label: t("setting.access-token-section.create-dialog.duration-8h"),
value: 3600 * 8,
label: t("setting.access-token-section.create-dialog.duration-1m"),
value: 30,
},
{
label: t("setting.access-token-section.create-dialog.duration-1m"),
value: 3600 * 24 * 30,
label: "90 Days",
value: 90,
},
{
label: t("setting.access-token-section.create-dialog.duration-never"),
@ -74,16 +74,14 @@ function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) {
try {
requestState.setLoading();
const created = await userServiceClient.createUserAccessToken({
const response = await userServiceClient.createPersonalAccessToken({
parent: currentUser.name,
accessToken: {
description: state.description,
expiresAt: state.expiration ? timestampFromDate(new Date(Date.now() + state.expiration * 1000)) : undefined,
},
description: state.description,
expiresInDays: state.expiration,
});
requestState.setFinish();
onSuccess(created);
onSuccess(response);
onOpenChange(false);
} catch (error: any) {
toast.error(error.message);

View File

@ -1,8 +1,10 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { ConnectError } from "@connectrpc/connect";
import { LoaderIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { setAccessToken } from "@/auth-state";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { authServiceClient } from "@/connect";
@ -45,12 +47,16 @@ const PasswordSignInForm = observer(() => {
try {
actionBtnLoadingState.setLoading();
await authServiceClient.createSession({
const response = await authServiceClient.signIn({
credentials: {
case: "passwordCredentials",
value: { username, password },
},
});
// Store access token from login response
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
await initialUserStore();
navigateTo("/");
} catch (error: any) {

View File

@ -1,6 +1,6 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import copy from "copy-to-clipboard";
import { ClipboardIcon, PlusIcon, TrashIcon } from "lucide-react";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog";
@ -8,66 +8,65 @@ import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useDialog } from "@/hooks/useDialog";
import { UserAccessToken } from "@/types/proto/api/v1/user_service_pb";
import { CreatePersonalAccessTokenResponse, PersonalAccessToken } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import CreateAccessTokenDialog from "../CreateAccessTokenDialog";
import SettingTable from "./SettingTable";
const listAccessTokens = async (parent: string) => {
const { accessTokens } = await userServiceClient.listUserAccessTokens({ parent });
return accessTokens.sort(
const { personalAccessTokens } = await userServiceClient.listPersonalAccessTokens({ parent });
return personalAccessTokens.sort(
(a, b) =>
((b.issuedAt ? timestampDate(b.issuedAt) : undefined)?.getTime() ?? 0) -
((a.issuedAt ? timestampDate(a.issuedAt) : undefined)?.getTime() ?? 0),
((b.createdAt ? timestampDate(b.createdAt) : undefined)?.getTime() ?? 0) -
((a.createdAt ? timestampDate(a.createdAt) : undefined)?.getTime() ?? 0),
);
};
const AccessTokenSection = () => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [userAccessTokens, setUserAccessTokens] = useState<UserAccessToken[]>([]);
const [personalAccessTokens, setPersonalAccessTokens] = useState<PersonalAccessToken[]>([]);
const createTokenDialog = useDialog();
const [deleteTarget, setDeleteTarget] = useState<UserAccessToken | undefined>(undefined);
const [deleteTarget, setDeleteTarget] = useState<PersonalAccessToken | undefined>(undefined);
useEffect(() => {
listAccessTokens(currentUser.name).then((accessTokens) => {
setUserAccessTokens(accessTokens);
listAccessTokens(currentUser.name).then((tokens) => {
setPersonalAccessTokens(tokens);
});
}, []);
const handleCreateAccessTokenDialogConfirm = async (created: UserAccessToken) => {
const accessTokens = await listAccessTokens(currentUser.name);
setUserAccessTokens(accessTokens);
toast.success(t("setting.access-token-section.create-dialog.access-token-created", { description: created.description }));
const handleCreateAccessTokenDialogConfirm = async (response: CreatePersonalAccessTokenResponse) => {
const tokens = await listAccessTokens(currentUser.name);
setPersonalAccessTokens(tokens);
// Copy the token to clipboard - this is the only time it will be shown
if (response.token) {
copy(response.token);
toast.success(t("setting.access-token-section.access-token-copied-to-clipboard"));
}
toast.success(
t("setting.access-token-section.create-dialog.access-token-created", {
description: response.personalAccessToken?.description ?? "",
}),
);
};
const handleCreateToken = () => {
createTokenDialog.open();
};
const copyAccessToken = (accessToken: string) => {
copy(accessToken);
toast.success(t("setting.access-token-section.access-token-copied-to-clipboard"));
};
const handleDeleteAccessToken = async (userAccessToken: UserAccessToken) => {
setDeleteTarget(userAccessToken);
const handleDeleteAccessToken = async (token: PersonalAccessToken) => {
setDeleteTarget(token);
};
const confirmDeleteAccessToken = async () => {
if (!deleteTarget) return;
const { name: tokenName, description } = deleteTarget;
await userServiceClient.deleteUserAccessToken({ name: tokenName });
// Filter by stable resource name to avoid ambiguity with duplicate token strings
setUserAccessTokens((prev) => prev.filter((token) => token.name !== tokenName));
await userServiceClient.deletePersonalAccessToken({ name: tokenName });
setPersonalAccessTokens((prev) => prev.filter((token) => token.name !== tokenName));
setDeleteTarget(undefined);
toast.success(t("setting.access-token-section.access-token-deleted", { description }));
};
const getFormatedAccessToken = (accessToken: string) => {
return `${accessToken.slice(0, 4)}****${accessToken.slice(-4)}`;
};
return (
<div className="w-full flex flex-col gap-2">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
@ -83,32 +82,20 @@ const AccessTokenSection = () => {
<SettingTable
columns={[
{
key: "accessToken",
header: t("setting.access-token-section.token"),
render: (_, token: UserAccessToken) => (
<div className="flex items-center gap-1">
<span className="font-mono text-foreground">{getFormatedAccessToken(token.accessToken)}</span>
<Button variant="ghost" size="sm" onClick={() => copyAccessToken(token.accessToken)}>
<ClipboardIcon className="w-4 h-auto text-muted-foreground" />
</Button>
</div>
),
},
{
key: "description",
header: t("common.description"),
render: (_, token: UserAccessToken) => <span className="text-foreground">{token.description}</span>,
render: (_, token: PersonalAccessToken) => <span className="text-foreground">{token.description}</span>,
},
{
key: "issuedAt",
key: "createdAt",
header: t("setting.access-token-section.create-dialog.created-at"),
render: (_, token: UserAccessToken) => (token.issuedAt ? timestampDate(token.issuedAt) : undefined)?.toLocaleString(),
render: (_, token: PersonalAccessToken) => (token.createdAt ? timestampDate(token.createdAt) : undefined)?.toLocaleString(),
},
{
key: "expiresAt",
header: t("setting.access-token-section.create-dialog.expires-at"),
render: (_, token: UserAccessToken) =>
render: (_, token: PersonalAccessToken) =>
(token.expiresAt ? timestampDate(token.expiresAt) : undefined)?.toLocaleString() ??
t("setting.access-token-section.create-dialog.duration-never"),
},
@ -116,14 +103,14 @@ const AccessTokenSection = () => {
key: "actions",
header: "",
className: "text-right",
render: (_, token: UserAccessToken) => (
render: (_, token: PersonalAccessToken) => (
<Button variant="ghost" size="sm" onClick={() => handleDeleteAccessToken(token)}>
<TrashIcon className="text-destructive w-4 h-auto" />
</Button>
),
},
]}
data={userAccessTokens}
data={personalAccessTokens}
emptyMessage="No access tokens found"
getRowKey={(token) => token.name}
/>

View File

@ -6,12 +6,12 @@ import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import useCurrentUser from "@/hooks/useCurrentUser";
import { UserSession } from "@/types/proto/api/v1/user_service_pb";
import { Session } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import SettingTable from "./SettingTable";
const listUserSessions = async (parent: string) => {
const { sessions } = await userServiceClient.listUserSessions({ parent });
const listSessions = async (parent: string) => {
const { sessions } = await userServiceClient.listSessions({ parent });
return sessions.sort(
(a, b) =>
((b.lastAccessedTime ? timestampDate(b.lastAccessedTime) : undefined)?.getTime() ?? 0) -
@ -22,23 +22,23 @@ const listUserSessions = async (parent: string) => {
const UserSessionsSection = () => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [userSessions, setUserSessions] = useState<UserSession[]>([]);
const [revokeTarget, setRevokeTarget] = useState<UserSession | undefined>(undefined);
const [sessions, setSessions] = useState<Session[]>([]);
const [revokeTarget, setRevokeTarget] = useState<Session | undefined>(undefined);
useEffect(() => {
listUserSessions(currentUser.name).then((sessions) => {
setUserSessions(sessions);
listSessions(currentUser.name).then((sessions) => {
setSessions(sessions);
});
}, []);
const handleRevokeSession = async (userSession: UserSession) => {
setRevokeTarget(userSession);
const handleRevokeSession = async (session: Session) => {
setRevokeTarget(session);
};
const confirmRevokeSession = async () => {
if (!revokeTarget) return;
await userServiceClient.revokeUserSession({ name: revokeTarget.name });
setUserSessions(userSessions.filter((session) => session.sessionId !== revokeTarget.sessionId));
await userServiceClient.revokeSession({ name: revokeTarget.name });
setSessions(sessions.filter((session) => session.sessionId !== revokeTarget.sessionId));
toast.success(t("setting.user-sessions-section.session-revoked"));
setRevokeTarget(undefined);
};
@ -59,7 +59,7 @@ const UserSessionsSection = () => {
}
};
const formatDeviceInfo = (clientInfo: UserSession["clientInfo"]) => {
const formatDeviceInfo = (clientInfo: Session["clientInfo"]) => {
if (!clientInfo) return "Unknown Device";
const parts = [];
@ -69,10 +69,10 @@ const UserSessionsSection = () => {
return parts.length > 0 ? parts.join(" • ") : "Unknown Device";
};
const isCurrentSession = (session: UserSession) => {
const isCurrentSession = (session: Session) => {
// A simple heuristic: the most recently accessed session is likely the current one
if (userSessions.length === 0) return false;
const mostRecent = userSessions[0];
if (sessions.length === 0) return false;
const mostRecent = sessions[0];
return session.sessionId === mostRecent.sessionId;
};
@ -88,7 +88,7 @@ const UserSessionsSection = () => {
{
key: "device",
header: t("setting.user-sessions-section.device"),
render: (_, session: UserSession) => (
render: (_, session: Session) => (
<div className="flex items-center space-x-3">
{getDeviceIcon(session.clientInfo?.deviceType || "")}
<div className="flex flex-col">
@ -109,7 +109,7 @@ const UserSessionsSection = () => {
{
key: "lastAccessedTime",
header: t("setting.user-sessions-section.last-active"),
render: (_, session: UserSession) => (
render: (_, session: Session) => (
<div className="flex items-center space-x-1">
<ClockIcon className="w-4 h-4" />
<span>{(session.lastAccessedTime ? timestampDate(session.lastAccessedTime) : undefined)?.toLocaleString()}</span>
@ -120,7 +120,7 @@ const UserSessionsSection = () => {
key: "actions",
header: "",
className: "text-right",
render: (_, session: UserSession) => (
render: (_, session: Session) => (
<Button
variant="ghost"
size="sm"
@ -137,7 +137,7 @@ const UserSessionsSection = () => {
),
},
]}
data={userSessions}
data={sessions}
emptyMessage={t("setting.user-sessions-section.no-sessions")}
getRowKey={(session) => session.sessionId}
/>

View File

@ -48,7 +48,7 @@ const UserMenu = observer((props: Props) => {
};
const handleSignOut = async () => {
await authServiceClient.deleteSession({});
await authServiceClient.signOut({});
// Clear user-specific localStorage items (e.g., drafts)
// Preserve app-wide settings like theme

View File

@ -1,5 +1,6 @@
import { createClient } from "@connectrpc/connect";
import { createClient, Interceptor } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { getAccessToken, setAccessToken } from "./auth-state";
import { ActivityService } from "./types/proto/api/v1/activity_service_pb";
import { AttachmentService } from "./types/proto/api/v1/attachment_service_pb";
import { AuthService } from "./types/proto/api/v1/auth_service_pb";
@ -9,10 +10,71 @@ import { MemoService } from "./types/proto/api/v1/memo_service_pb";
import { ShortcutService } from "./types/proto/api/v1/shortcut_service_pb";
import { UserService } from "./types/proto/api/v1/user_service_pb";
let isRefreshing = false;
let refreshPromise: Promise<void> | null = null;
// Auth interceptor that attaches access token and handles 401 errors by refreshing
const authInterceptor: Interceptor = (next) => async (req) => {
// Add access token to request if available
const token = getAccessToken();
if (token) {
req.header.set("Authorization", `Bearer ${token}`);
}
try {
return await next(req);
} catch (error: any) {
// Handle unauthenticated error - try to refresh token
if (error.code === "unauthenticated" && !req.header.get("X-Retry")) {
// Prevent concurrent refresh attempts
if (!isRefreshing) {
isRefreshing = true;
refreshPromise = refreshAccessToken();
}
try {
await refreshPromise;
isRefreshing = false;
refreshPromise = null;
// Retry with new token
const newToken = getAccessToken();
if (newToken) {
req.header.set("Authorization", `Bearer ${newToken}`);
req.header.set("X-Retry", "true");
return await next(req);
}
} catch (refreshError) {
isRefreshing = false;
refreshPromise = null;
// Refresh failed - redirect to login
window.location.href = "/auth";
throw refreshError;
}
}
throw error;
}
};
async function refreshAccessToken(): Promise<void> {
const response = await fetch("/api/v1/auth/refresh", {
method: "POST",
credentials: "include", // Include HttpOnly cookies with refresh token
});
if (!response.ok) {
throw new Error("Failed to refresh token");
}
const data = await response.json();
setAccessToken(data.accessToken, new Date(data.expiresAt));
}
const transport = createConnectTransport({
baseUrl: window.location.origin,
// Use binary protobuf format for better performance (smaller payloads, faster serialization)
useBinaryFormat: true,
interceptors: [authInterceptor],
});
// Core service clients

View File

@ -1,8 +1,10 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { ConnectError } from "@connectrpc/connect";
import { LoaderIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { setAccessToken } from "@/auth-state";
import { authServiceClient } from "@/connect";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
@ -71,7 +73,7 @@ const AuthCallback = observer(() => {
(async () => {
try {
await authServiceClient.createSession({
const response = await authServiceClient.signIn({
credentials: {
case: "ssoCredentials",
value: {
@ -82,6 +84,10 @@ const AuthCallback = observer(() => {
},
},
});
// Store access token from login response
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
setState({
loading: false,
errorMessage: "",

View File

@ -1,10 +1,12 @@
import { create } from "@bufbuild/protobuf";
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { ConnectError } from "@connectrpc/connect";
import { LoaderIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { Link } from "react-router-dom";
import { setAccessToken } from "@/auth-state";
import AuthFooter from "@/components/AuthFooter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -56,12 +58,16 @@ const SignUp = observer(() => {
role: User_Role.USER,
});
await userServiceClient.createUser({ user });
await authServiceClient.createSession({
const response = await authServiceClient.signIn({
credentials: {
case: "passwordCredentials",
value: { username, password },
},
});
// Store access token from login response
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
await initialUserStore();
navigateTo("/");
} catch (error: any) {

View File

@ -2,6 +2,7 @@ import { create } from "@bufbuild/protobuf";
import { FieldMaskSchema } from "@bufbuild/protobuf/wkt";
import { uniqueId } from "lodash-es";
import { computed, makeAutoObservable } from "mobx";
import { clearAccessToken, setAccessToken } from "@/auth-state";
import { authServiceClient, shortcutServiceClient, userServiceClient } from "@/connect";
import { Shortcut } from "@/types/proto/api/v1/shortcut_service_pb";
import {
@ -322,13 +323,21 @@ const userStore = (() => {
// 1. Fetch current authenticated user session
// 2. Set current user in store (required for subsequent calls)
// 3. Fetch user settings (depends on currentUser being set)
//
// Auth flow:
// - On first call, GetCurrentSession has no access token
// - The interceptor will automatically call RefreshToken using the HttpOnly refresh cookie
// - If refresh succeeds, GetCurrentSession is retried with the new access token
// - If refresh fails (no cookie or expired), user needs to login
export const initialUserStore = async () => {
try {
// Step 1: Authenticate and get current user
const { user: currentUser } = await authServiceClient.getCurrentSession({});
// The interceptor will handle token refresh if needed
const { user: currentUser } = await authServiceClient.getCurrentUser({});
if (!currentUser) {
// No authenticated user - clear state
clearAccessToken();
userStore.state.setPartial({
currentUser: undefined,
userGeneralSetting: undefined,
@ -351,8 +360,44 @@ export const initialUserStore = async () => {
// CRITICAL: This must happen after currentUser is set in step 2
// The fetchUserSettings() and fetchUserStats() methods check state.currentUser internally
await Promise.all([userStore.fetchUserSettings(), userStore.fetchUserStats()]);
} catch (error) {
} catch (error: any) {
// Auth failed (no refresh token, expired, or other error)
// Clear state and let user login again
console.error("Failed to initialize user store:", error);
clearAccessToken();
userStore.state.setPartial({
currentUser: undefined,
userGeneralSetting: undefined,
userMapByName: {},
});
}
};
// Logout function that clears tokens and state
// This calls DeleteSession which:
// 1. Revokes the refresh token in the database
// 2. Clears both session and refresh token cookies
// We then clear the in-memory access token and reset the store state
export const logout = async () => {
try {
await authServiceClient.signOut({});
} catch (error) {
// Log error but continue with local cleanup
console.error("Failed to delete session on server:", error);
} finally {
// Always clear local state, even if server call fails
clearAccessToken();
userStore.state.setPartial({
currentUser: undefined,
userGeneralSetting: undefined,
userSessionsSetting: undefined,
userAccessTokensSetting: undefined,
userWebhooksSetting: undefined,
shortcuts: [],
notifications: [],
userMapByName: {},
userStatsByName: {},
});
}
};

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/activity_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/auth_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */
@ -16,91 +16,83 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/auth_service.proto.
*/
export const file_api_v1_auth_service: GenFile = /*@__PURE__*/
fileDesc("ChlhcGkvdjEvYXV0aF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEiGgoYR2V0Q3VycmVudFNlc3Npb25SZXF1ZXN0InMKGUdldEN1cnJlbnRTZXNzaW9uUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyEjQKEGxhc3RfYWNjZXNzZWRfYXQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoEDChRDcmVhdGVTZXNzaW9uUmVxdWVzdBJWChRwYXNzd29yZF9jcmVkZW50aWFscxgBIAEoCzI2Lm1lbW9zLmFwaS52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdC5QYXNzd29yZENyZWRlbnRpYWxzSAASTAoPc3NvX2NyZWRlbnRpYWxzGAIgASgLMjEubWVtb3MuYXBpLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0LlNTT0NyZWRlbnRpYWxzSAAaQwoTUGFzc3dvcmRDcmVkZW50aWFscxIVCgh1c2VybmFtZRgBIAEoCUID4EECEhUKCHBhc3N3b3JkGAIgASgJQgPgQQIabwoOU1NPQ3JlZGVudGlhbHMSEwoGaWRwX2lkGAEgASgFQgPgQQISEQoEY29kZRgCIAEoCUID4EECEhkKDHJlZGlyZWN0X3VyaRgDIAEoCUID4EECEhoKDWNvZGVfdmVyaWZpZXIYBCABKAlCA+BBAUINCgtjcmVkZW50aWFscyJvChVDcmVhdGVTZXNzaW9uUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyEjQKEGxhc3RfYWNjZXNzZWRfYXQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhYKFERlbGV0ZVNlc3Npb25SZXF1ZXN0MosDCgtBdXRoU2VydmljZRKLAQoRR2V0Q3VycmVudFNlc3Npb24SJi5tZW1vcy5hcGkudjEuR2V0Q3VycmVudFNlc3Npb25SZXF1ZXN0GicubWVtb3MuYXBpLnYxLkdldEN1cnJlbnRTZXNzaW9uUmVzcG9uc2UiJYLT5JMCHxIdL2FwaS92MS9hdXRoL3Nlc3Npb25zL2N1cnJlbnQSegoNQ3JlYXRlU2Vzc2lvbhIiLm1lbW9zLmFwaS52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdBojLm1lbW9zLmFwaS52MS5DcmVhdGVTZXNzaW9uUmVzcG9uc2UiIILT5JMCGjoBKiIVL2FwaS92MS9hdXRoL3Nlc3Npb25zEnIKDURlbGV0ZVNlc3Npb24SIi5tZW1vcy5hcGkudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJYLT5JMCHyodL2FwaS92MS9hdXRoL3Nlc3Npb25zL2N1cnJlbnRCqAEKEGNvbS5tZW1vcy5hcGkudjFCEEF1dGhTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_user_service, file_google_api_annotations, file_google_api_field_behavior, file_google_protobuf_empty, file_google_protobuf_timestamp]);
fileDesc("ChlhcGkvdjEvYXV0aF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEiFwoVR2V0Q3VycmVudFVzZXJSZXF1ZXN0IjoKFkdldEN1cnJlbnRVc2VyUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyIuwCCg1TaWduSW5SZXF1ZXN0Ek8KFHBhc3N3b3JkX2NyZWRlbnRpYWxzGAEgASgLMi8ubWVtb3MuYXBpLnYxLlNpZ25JblJlcXVlc3QuUGFzc3dvcmRDcmVkZW50aWFsc0gAEkUKD3Nzb19jcmVkZW50aWFscxgCIAEoCzIqLm1lbW9zLmFwaS52MS5TaWduSW5SZXF1ZXN0LlNTT0NyZWRlbnRpYWxzSAAaQwoTUGFzc3dvcmRDcmVkZW50aWFscxIVCgh1c2VybmFtZRgBIAEoCUID4EECEhUKCHBhc3N3b3JkGAIgASgJQgPgQQIabwoOU1NPQ3JlZGVudGlhbHMSEwoGaWRwX2lkGAEgASgFQgPgQQISEQoEY29kZRgCIAEoCUID4EECEhkKDHJlZGlyZWN0X3VyaRgDIAEoCUID4EECEhoKDWNvZGVfdmVyaWZpZXIYBCABKAlCA+BBAUINCgtjcmVkZW50aWFscyKFAQoOU2lnbkluUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhQKDGFjY2Vzc190b2tlbhgCIAEoCRI7ChdhY2Nlc3NfdG9rZW5fZXhwaXJlc19hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiEAoOU2lnbk91dFJlcXVlc3QiFQoTUmVmcmVzaFRva2VuUmVxdWVzdCJcChRSZWZyZXNoVG9rZW5SZXNwb25zZRIUCgxhY2Nlc3NfdG9rZW4YASABKAkSLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAyvwMKC0F1dGhTZXJ2aWNlEnQKDkdldEN1cnJlbnRVc2VyEiMubWVtb3MuYXBpLnYxLkdldEN1cnJlbnRVc2VyUmVxdWVzdBokLm1lbW9zLmFwaS52MS5HZXRDdXJyZW50VXNlclJlc3BvbnNlIheC0+STAhESDy9hcGkvdjEvYXV0aC9tZRJjCgZTaWduSW4SGy5tZW1vcy5hcGkudjEuU2lnbkluUmVxdWVzdBocLm1lbW9zLmFwaS52MS5TaWduSW5SZXNwb25zZSIegtPkkwIYOgEqIhMvYXBpL3YxL2F1dGgvc2lnbmluEl0KB1NpZ25PdXQSHC5tZW1vcy5hcGkudjEuU2lnbk91dFJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiHILT5JMCFiIUL2FwaS92MS9hdXRoL3NpZ25vdXQSdgoMUmVmcmVzaFRva2VuEiEubWVtb3MuYXBpLnYxLlJlZnJlc2hUb2tlblJlcXVlc3QaIi5tZW1vcy5hcGkudjEuUmVmcmVzaFRva2VuUmVzcG9uc2UiH4LT5JMCGToBKiIUL2FwaS92MS9hdXRoL3JlZnJlc2hCqAEKEGNvbS5tZW1vcy5hcGkudjFCEEF1dGhTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_user_service, file_google_api_annotations, file_google_api_field_behavior, file_google_protobuf_empty, file_google_protobuf_timestamp]);
/**
* @generated from message memos.api.v1.GetCurrentSessionRequest
* @generated from message memos.api.v1.GetCurrentUserRequest
*/
export type GetCurrentSessionRequest = Message<"memos.api.v1.GetCurrentSessionRequest"> & {
export type GetCurrentUserRequest = Message<"memos.api.v1.GetCurrentUserRequest"> & {
};
/**
* Describes the message memos.api.v1.GetCurrentSessionRequest.
* Use `create(GetCurrentSessionRequestSchema)` to create a new message.
* Describes the message memos.api.v1.GetCurrentUserRequest.
* Use `create(GetCurrentUserRequestSchema)` to create a new message.
*/
export const GetCurrentSessionRequestSchema: GenMessage<GetCurrentSessionRequest> = /*@__PURE__*/
export const GetCurrentUserRequestSchema: GenMessage<GetCurrentUserRequest> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 0);
/**
* @generated from message memos.api.v1.GetCurrentSessionResponse
* @generated from message memos.api.v1.GetCurrentUserResponse
*/
export type GetCurrentSessionResponse = Message<"memos.api.v1.GetCurrentSessionResponse"> & {
export type GetCurrentUserResponse = Message<"memos.api.v1.GetCurrentUserResponse"> & {
/**
* The authenticated user's information.
*
* @generated from field: memos.api.v1.User user = 1;
*/
user?: User;
/**
* Last time the session was accessed.
* Used for sliding expiration calculation (last_accessed_time + 2 weeks).
*
* @generated from field: google.protobuf.Timestamp last_accessed_at = 2;
*/
lastAccessedAt?: Timestamp;
};
/**
* Describes the message memos.api.v1.GetCurrentSessionResponse.
* Use `create(GetCurrentSessionResponseSchema)` to create a new message.
* Describes the message memos.api.v1.GetCurrentUserResponse.
* Use `create(GetCurrentUserResponseSchema)` to create a new message.
*/
export const GetCurrentSessionResponseSchema: GenMessage<GetCurrentSessionResponse> = /*@__PURE__*/
export const GetCurrentUserResponseSchema: GenMessage<GetCurrentUserResponse> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 1);
/**
* @generated from message memos.api.v1.CreateSessionRequest
* @generated from message memos.api.v1.SignInRequest
*/
export type CreateSessionRequest = Message<"memos.api.v1.CreateSessionRequest"> & {
export type SignInRequest = Message<"memos.api.v1.SignInRequest"> & {
/**
* Provide one authentication method (username/password or SSO).
* Required field to specify the authentication method.
* Authentication credentials. Provide one method.
*
* @generated from oneof memos.api.v1.CreateSessionRequest.credentials
* @generated from oneof memos.api.v1.SignInRequest.credentials
*/
credentials: {
/**
* Username and password authentication method.
* Username and password authentication.
*
* @generated from field: memos.api.v1.CreateSessionRequest.PasswordCredentials password_credentials = 1;
* @generated from field: memos.api.v1.SignInRequest.PasswordCredentials password_credentials = 1;
*/
value: CreateSessionRequest_PasswordCredentials;
value: SignInRequest_PasswordCredentials;
case: "passwordCredentials";
} | {
/**
* SSO provider authentication method.
* SSO provider authentication.
*
* @generated from field: memos.api.v1.CreateSessionRequest.SSOCredentials sso_credentials = 2;
* @generated from field: memos.api.v1.SignInRequest.SSOCredentials sso_credentials = 2;
*/
value: CreateSessionRequest_SSOCredentials;
value: SignInRequest_SSOCredentials;
case: "ssoCredentials";
} | { case: undefined; value?: undefined };
};
/**
* Describes the message memos.api.v1.CreateSessionRequest.
* Use `create(CreateSessionRequestSchema)` to create a new message.
* Describes the message memos.api.v1.SignInRequest.
* Use `create(SignInRequestSchema)` to create a new message.
*/
export const CreateSessionRequestSchema: GenMessage<CreateSessionRequest> = /*@__PURE__*/
export const SignInRequestSchema: GenMessage<SignInRequest> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 2);
/**
* Nested message for password-based authentication credentials.
*
* @generated from message memos.api.v1.CreateSessionRequest.PasswordCredentials
* @generated from message memos.api.v1.SignInRequest.PasswordCredentials
*/
export type CreateSessionRequest_PasswordCredentials = Message<"memos.api.v1.CreateSessionRequest.PasswordCredentials"> & {
export type SignInRequest_PasswordCredentials = Message<"memos.api.v1.SignInRequest.PasswordCredentials"> & {
/**
* The username to sign in with.
* Required field for password-based authentication.
*
* @generated from field: string username = 1;
*/
@ -108,7 +100,6 @@ export type CreateSessionRequest_PasswordCredentials = Message<"memos.api.v1.Cre
/**
* The password to sign in with.
* Required field for password-based authentication.
*
* @generated from field: string password = 2;
*/
@ -116,21 +107,20 @@ export type CreateSessionRequest_PasswordCredentials = Message<"memos.api.v1.Cre
};
/**
* Describes the message memos.api.v1.CreateSessionRequest.PasswordCredentials.
* Use `create(CreateSessionRequest_PasswordCredentialsSchema)` to create a new message.
* Describes the message memos.api.v1.SignInRequest.PasswordCredentials.
* Use `create(SignInRequest_PasswordCredentialsSchema)` to create a new message.
*/
export const CreateSessionRequest_PasswordCredentialsSchema: GenMessage<CreateSessionRequest_PasswordCredentials> = /*@__PURE__*/
export const SignInRequest_PasswordCredentialsSchema: GenMessage<SignInRequest_PasswordCredentials> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 2, 0);
/**
* Nested message for SSO authentication credentials.
*
* @generated from message memos.api.v1.CreateSessionRequest.SSOCredentials
* @generated from message memos.api.v1.SignInRequest.SSOCredentials
*/
export type CreateSessionRequest_SSOCredentials = Message<"memos.api.v1.CreateSessionRequest.SSOCredentials"> & {
export type SignInRequest_SSOCredentials = Message<"memos.api.v1.SignInRequest.SSOCredentials"> & {
/**
* The ID of the SSO provider.
* Required field to identify the SSO provider.
*
* @generated from field: int32 idp_id = 1;
*/
@ -138,7 +128,6 @@ export type CreateSessionRequest_SSOCredentials = Message<"memos.api.v1.CreateSe
/**
* The authorization code from the SSO provider.
* Required field for completing the SSO flow.
*
* @generated from field: string code = 2;
*/
@ -146,7 +135,6 @@ export type CreateSessionRequest_SSOCredentials = Message<"memos.api.v1.CreateSe
/**
* The redirect URI used in the SSO flow.
* Required field for security validation.
*
* @generated from field: string redirect_uri = 3;
*/
@ -154,7 +142,7 @@ export type CreateSessionRequest_SSOCredentials = Message<"memos.api.v1.CreateSe
/**
* The PKCE code verifier for enhanced security (RFC 7636).
* Optional field - if provided, enables PKCE flow protection against authorization code interception.
* Optional - enables PKCE flow protection against authorization code interception.
*
* @generated from field: string code_verifier = 4;
*/
@ -162,89 +150,150 @@ export type CreateSessionRequest_SSOCredentials = Message<"memos.api.v1.CreateSe
};
/**
* Describes the message memos.api.v1.CreateSessionRequest.SSOCredentials.
* Use `create(CreateSessionRequest_SSOCredentialsSchema)` to create a new message.
* Describes the message memos.api.v1.SignInRequest.SSOCredentials.
* Use `create(SignInRequest_SSOCredentialsSchema)` to create a new message.
*/
export const CreateSessionRequest_SSOCredentialsSchema: GenMessage<CreateSessionRequest_SSOCredentials> = /*@__PURE__*/
export const SignInRequest_SSOCredentialsSchema: GenMessage<SignInRequest_SSOCredentials> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 2, 1);
/**
* @generated from message memos.api.v1.CreateSessionResponse
* @generated from message memos.api.v1.SignInResponse
*/
export type CreateSessionResponse = Message<"memos.api.v1.CreateSessionResponse"> & {
export type SignInResponse = Message<"memos.api.v1.SignInResponse"> & {
/**
* The authenticated user information.
* The authenticated user's information.
*
* @generated from field: memos.api.v1.User user = 1;
*/
user?: User;
/**
* Last time the session was accessed.
* Used for sliding expiration calculation (last_accessed_time + 2 weeks).
* The short-lived access token for API requests.
* Store in memory only, not in localStorage.
*
* @generated from field: google.protobuf.Timestamp last_accessed_at = 2;
* @generated from field: string access_token = 2;
*/
lastAccessedAt?: Timestamp;
accessToken: string;
/**
* When the access token expires.
* Client should call RefreshToken before this time.
*
* @generated from field: google.protobuf.Timestamp access_token_expires_at = 3;
*/
accessTokenExpiresAt?: Timestamp;
};
/**
* Describes the message memos.api.v1.CreateSessionResponse.
* Use `create(CreateSessionResponseSchema)` to create a new message.
* Describes the message memos.api.v1.SignInResponse.
* Use `create(SignInResponseSchema)` to create a new message.
*/
export const CreateSessionResponseSchema: GenMessage<CreateSessionResponse> = /*@__PURE__*/
export const SignInResponseSchema: GenMessage<SignInResponse> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 3);
/**
* @generated from message memos.api.v1.DeleteSessionRequest
* @generated from message memos.api.v1.SignOutRequest
*/
export type DeleteSessionRequest = Message<"memos.api.v1.DeleteSessionRequest"> & {
export type SignOutRequest = Message<"memos.api.v1.SignOutRequest"> & {
};
/**
* Describes the message memos.api.v1.DeleteSessionRequest.
* Use `create(DeleteSessionRequestSchema)` to create a new message.
* Describes the message memos.api.v1.SignOutRequest.
* Use `create(SignOutRequestSchema)` to create a new message.
*/
export const DeleteSessionRequestSchema: GenMessage<DeleteSessionRequest> = /*@__PURE__*/
export const SignOutRequestSchema: GenMessage<SignOutRequest> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 4);
/**
* @generated from message memos.api.v1.RefreshTokenRequest
*/
export type RefreshTokenRequest = Message<"memos.api.v1.RefreshTokenRequest"> & {
};
/**
* Describes the message memos.api.v1.RefreshTokenRequest.
* Use `create(RefreshTokenRequestSchema)` to create a new message.
*/
export const RefreshTokenRequestSchema: GenMessage<RefreshTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 5);
/**
* @generated from message memos.api.v1.RefreshTokenResponse
*/
export type RefreshTokenResponse = Message<"memos.api.v1.RefreshTokenResponse"> & {
/**
* The new short-lived access token.
*
* @generated from field: string access_token = 1;
*/
accessToken: string;
/**
* When the access token expires.
*
* @generated from field: google.protobuf.Timestamp expires_at = 2;
*/
expiresAt?: Timestamp;
};
/**
* Describes the message memos.api.v1.RefreshTokenResponse.
* Use `create(RefreshTokenResponseSchema)` to create a new message.
*/
export const RefreshTokenResponseSchema: GenMessage<RefreshTokenResponse> = /*@__PURE__*/
messageDesc(file_api_v1_auth_service, 6);
/**
* @generated from service memos.api.v1.AuthService
*/
export const AuthService: GenService<{
/**
* GetCurrentSession returns the current active session information.
* This method is idempotent and safe, suitable for checking current session state.
* GetCurrentUser returns the authenticated user's information.
* Validates the access token and returns user details.
* Similar to OIDC's /userinfo endpoint.
*
* @generated from rpc memos.api.v1.AuthService.GetCurrentSession
* @generated from rpc memos.api.v1.AuthService.GetCurrentUser
*/
getCurrentSession: {
getCurrentUser: {
methodKind: "unary";
input: typeof GetCurrentSessionRequestSchema;
output: typeof GetCurrentSessionResponseSchema;
input: typeof GetCurrentUserRequestSchema;
output: typeof GetCurrentUserResponseSchema;
},
/**
* CreateSession authenticates a user and creates a new session.
* Returns the authenticated user information upon successful authentication.
* SignIn authenticates a user with credentials and returns tokens.
* On success, returns an access token and sets a refresh token cookie.
* Supports password-based and SSO authentication methods.
*
* @generated from rpc memos.api.v1.AuthService.CreateSession
* @generated from rpc memos.api.v1.AuthService.SignIn
*/
createSession: {
signIn: {
methodKind: "unary";
input: typeof CreateSessionRequestSchema;
output: typeof CreateSessionResponseSchema;
input: typeof SignInRequestSchema;
output: typeof SignInResponseSchema;
},
/**
* DeleteSession terminates the current user session.
* This is an idempotent operation that invalidates the user's authentication.
* SignOut terminates the user's authentication.
* Revokes the refresh token and clears the authentication cookie.
*
* @generated from rpc memos.api.v1.AuthService.DeleteSession
* @generated from rpc memos.api.v1.AuthService.SignOut
*/
deleteSession: {
signOut: {
methodKind: "unary";
input: typeof DeleteSessionRequestSchema;
input: typeof SignOutRequestSchema;
output: typeof EmptySchema;
},
/**
* RefreshToken exchanges a valid refresh token for a new access token.
* The refresh token is read from the HttpOnly cookie.
* Returns a new short-lived access token.
*
* @generated from rpc memos.api.v1.AuthService.RefreshToken
*/
refreshToken: {
methodKind: "unary";
input: typeof RefreshTokenRequestSchema;
output: typeof RefreshTokenResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_api_v1_auth_service, 0);

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/common.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/idp_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/instance_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/memo_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

View File

@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file api/v1/shortcut_service.proto (package memos.api.v1, syntax proto3)
/* eslint-disable */

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/annotations.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/client.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/field_behavior.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/http.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/launch_stage.proto (package google.api, syntax proto3)
/* eslint-disable */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// @generated by protoc-gen-es v2.10.1 with parameter "target=ts"
// @generated by protoc-gen-es v2.10.2 with parameter "target=ts"
// @generated from file google/api/resource.proto (package google.api, syntax proto3)
/* eslint-disable */