Skip to content
11 changes: 6 additions & 5 deletions internal/api/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"go.mongodb.org/mongo-driver/v2/bson"

"paperdebugger/internal/accesscontrol"
"paperdebugger/internal/libs/jwt"
"paperdebugger/internal/libs/shared"
Expand All @@ -12,26 +13,26 @@ import (

func parseUserActor(ctx context.Context, token string, userService *services.UserService) (*accesscontrol.Actor, error) {
if len(token) == 0 {
return nil, shared.ErrInvalidToken()
return nil, shared.ErrInvalidToken("Authentication token is required")
}

claims, err := jwt.VerifyJwtToken(token)
if err != nil {
return nil, shared.ErrInvalidToken(err)
return nil, shared.ErrInvalidToken(err.Error())
}

if len(claims.Audience) == 0 || claims.Audience[0] != "paperdebugger/user" {
return nil, shared.ErrInvalidActor()
return nil, shared.ErrInvalidActor("Invalid token audience")
}

actorID, err := bson.ObjectIDFromHex(claims.Subject)
if err != nil {
return nil, shared.ErrInvalidActor()
return nil, shared.ErrInvalidActor("Invalid actor ID format")
}

_, err = userService.GetUserByID(ctx, actorID)
if err != nil {
return nil, shared.ErrInvalidUser(err)
return nil, shared.ErrInvalidUser(err.Error())
}

return &accesscontrol.Actor{ID: actorID}, nil
Expand Down
14 changes: 13 additions & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (s *Server) errorHandler() func(ctx context.Context, mux *runtime.ServeMux,

err := &sharedv1.Error{
Code: errCode,
Message: reqError.Error(),
Message: cleanErrorMessage(reqError),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(shared.GetHTTPCode(reqError))
Expand All @@ -182,3 +182,15 @@ func (s *Server) errorHandler() func(ctx context.Context, mux *runtime.ServeMux,
}
}
}

// cleanErrorMessage removes technical gRPC error prefixes from error messages
func cleanErrorMessage(err error) string {
msg := err.Error()
// Remove "rpc error: code = Code(XXXX) desc = " prefix
if strings.HasPrefix(msg, "rpc error:") {
if idx := strings.Index(msg, "desc = "); idx != -1 {
return msg[idx+7:]
}
}
return msg
}
26 changes: 25 additions & 1 deletion internal/libs/shared/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,29 @@ import (
"fmt"
"net/http"

sharedv1 "paperdebugger/pkg/gen/api/shared/v1"

"github.com/samber/lo"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
sharedv1 "paperdebugger/pkg/gen/api/shared/v1"
)

// errorCodeMessages provides default user-friendly messages for each error code
var errorCodeMessages = map[sharedv1.ErrorCode]string{
sharedv1.ErrorCode_ERROR_CODE_UNSPECIFIED: "An unspecified error occurred",
sharedv1.ErrorCode_ERROR_CODE_UNKNOWN: "An unknown error occurred",
sharedv1.ErrorCode_ERROR_CODE_INTERNAL: "Internal server error",
sharedv1.ErrorCode_ERROR_CODE_BAD_REQUEST: "Bad request",
sharedv1.ErrorCode_ERROR_CODE_INVALID_LLM_RESPONSE: "Invalid LLM response",
sharedv1.ErrorCode_ERROR_CODE_RECORD_NOT_FOUND: "Record not found",
sharedv1.ErrorCode_ERROR_CODE_INVALID_CREDENTIAL: "Invalid credentials",
sharedv1.ErrorCode_ERROR_CODE_INVALID_TOKEN: "Invalid or missing authentication token",
sharedv1.ErrorCode_ERROR_CODE_INVALID_ACTOR: "Invalid actor or session",
sharedv1.ErrorCode_ERROR_CODE_PERMISSION_DENIED: "Permission denied",
sharedv1.ErrorCode_ERROR_CODE_INVALID_USER: "User not found or invalid",
sharedv1.ErrorCode_ERROR_CODE_PROJECT_OUT_OF_DATE: "Project is out of date",
}

var (
ErrUnknown = makeErrorFunc(sharedv1.ErrorCode_ERROR_CODE_UNKNOWN)
ErrInternal = makeErrorFunc(sharedv1.ErrorCode_ERROR_CODE_INTERNAL)
Expand Down Expand Up @@ -59,6 +76,13 @@ func makeErrorFunc(
detail := lo.FirstOrEmpty(details)
var errorMessage string
switch v := detail.(type) {
case nil:
// Use default message from errorCodeMessages when no details provided
if msg, ok := errorCodeMessages[errorCode]; ok {
errorMessage = msg
} else {
errorMessage = "An error occurred"
}
case error:
errorMessage = v.Error()
case interface{ String() string }:
Expand Down
6 changes: 3 additions & 3 deletions pkg/gen/api/auth/v1/auth.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions pkg/gen/api/auth/v1/auth.pb.gw.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions proto/auth/v1/auth.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ service AuthService {
option (google.api.http) = {
post: "/_pd/api/v1/auth/refresh"
body: "*"
additional_bindings {
post: "/_pd/api/v2/auth/refresh"
body: "*"
}
};
}
rpc Logout(LogoutRequest) returns (LogoutResponse) {
Expand Down
38 changes: 36 additions & 2 deletions webapp/_webapp/src/libs/apiclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,49 @@ class ApiClient {
const errorData = error.response?.data;
const errorPayload = fromJson(ErrorSchema, errorData);
if (!options?.ignoreErrorToast) {
const message = errorPayload.message.replace(/^rpc error: code = Code\(\d+\) desc = /, "");
errorToast(message + ` (${config.url})`, `Request Failed: ${ErrorCode[errorPayload.code]}`);
const message = this.cleanErrorMessage(errorPayload.message);
const title = this.getErrorTitle(errorPayload.code);
errorToast(message, title);
}
throw errorPayload;
}
throw error;
}
}

private cleanErrorMessage(msg: string): string {
// Remove technical gRPC prefixes, mirroring backend behavior:
// strip everything up to and including "desc = " when the message
// starts with "rpc error:" and contains "desc = ".
if (msg.startsWith("rpc error:")) {
const marker = "desc = ";
const idx = msg.indexOf(marker);
if (idx !== -1) {
return msg.slice(idx + marker.length);
}
}
return msg;
}

// Error titles aligned with backend errorCodeMessages (error.go) for consistency
private getErrorTitle(code: ErrorCode): string {
const titles: Record<ErrorCode, string> = {
[ErrorCode.UNSPECIFIED]: "An unspecified error occurred",
[ErrorCode.UNKNOWN]: "An unknown error occurred",
[ErrorCode.INVALID_TOKEN]: "Invalid or missing authentication token",
[ErrorCode.INVALID_ACTOR]: "Invalid actor or session",
[ErrorCode.INVALID_USER]: "User not found or invalid",
[ErrorCode.PERMISSION_DENIED]: "Permission denied",
[ErrorCode.RECORD_NOT_FOUND]: "Record not found",
[ErrorCode.BAD_REQUEST]: "Bad request",
[ErrorCode.INTERNAL]: "Internal server error",
[ErrorCode.INVALID_CREDENTIAL]: "Invalid credentials",
[ErrorCode.INVALID_LLM_RESPONSE]: "Invalid LLM response",
[ErrorCode.PROJECT_OUT_OF_DATE]: "Project is out of date",
};
return titles[code] || "Request Failed";
}

async get(url: string, params?: object, options?: RequestOptions): Promise<JsonValue> {
return this.requestWithErrorToast(
{
Expand Down
2 changes: 1 addition & 1 deletion webapp/_webapp/src/pkg/gen/apiclient/auth/v1/auth_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file auth/v1/auth.proto.
*/
export const file_auth_v1_auth: GenFile = /*@__PURE__*/
fileDesc("ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiLAoUTG9naW5CeUdvb2dsZVJlcXVlc3QSFAoMZ29vZ2xlX3Rva2VuGAEgASgJIj0KFUxvZ2luQnlHb29nbGVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAIgASgJIjAKFkxvZ2luQnlPdmVybGVhZlJlcXVlc3QSFgoOb3ZlcmxlYWZfdG9rZW4YASABKAkiPwoXTG9naW5CeU92ZXJsZWFmUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSIsChNSZWZyZXNoVG9rZW5SZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiPAoUUmVmcmVzaFRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSImCg1Mb2dvdXRSZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiEAoOTG9nb3V0UmVzcG9uc2Uy2wMKC0F1dGhTZXJ2aWNlEngKDUxvZ2luQnlHb29nbGUSHS5hdXRoLnYxLkxvZ2luQnlHb29nbGVSZXF1ZXN0Gh4uYXV0aC52MS5Mb2dpbkJ5R29vZ2xlUmVzcG9uc2UiKILT5JMCIjoBKiIdL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9nb29nbGUSgAEKD0xvZ2luQnlPdmVybGVhZhIfLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVxdWVzdBogLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVzcG9uc2UiKoLT5JMCJDoBKiIfL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9vdmVybGVhZhJwCgxSZWZyZXNoVG9rZW4SHC5hdXRoLnYxLlJlZnJlc2hUb2tlblJlcXVlc3QaHS5hdXRoLnYxLlJlZnJlc2hUb2tlblJlc3BvbnNlIiOC0+STAh06ASoiGC9fcGQvYXBpL3YxL2F1dGgvcmVmcmVzaBJdCgZMb2dvdXQSFi5hdXRoLnYxLkxvZ291dFJlcXVlc3QaFy5hdXRoLnYxLkxvZ291dFJlc3BvbnNlIiKC0+STAhw6ASoiFy9fcGQvYXBpL3YxL2F1dGgvbG9nb3V0Qn8KC2NvbS5hdXRoLnYxQglBdXRoUHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS9hdXRoL3YxO2F1dGh2MaICA0FYWKoCB0F1dGguVjHKAgdBdXRoXFYx4gITQXV0aFxWMVxHUEJNZXRhZGF0YeoCCEF1dGg6OlYxYgZwcm90bzM", [file_google_api_annotations]);
fileDesc("ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiLAoUTG9naW5CeUdvb2dsZVJlcXVlc3QSFAoMZ29vZ2xlX3Rva2VuGAEgASgJIj0KFUxvZ2luQnlHb29nbGVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAIgASgJIjAKFkxvZ2luQnlPdmVybGVhZlJlcXVlc3QSFgoOb3ZlcmxlYWZfdG9rZW4YASABKAkiPwoXTG9naW5CeU92ZXJsZWFmUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSIsChNSZWZyZXNoVG9rZW5SZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiPAoUUmVmcmVzaFRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSImCg1Mb2dvdXRSZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiEAoOTG9nb3V0UmVzcG9uc2Uy+wMKC0F1dGhTZXJ2aWNlEngKDUxvZ2luQnlHb29nbGUSHS5hdXRoLnYxLkxvZ2luQnlHb29nbGVSZXF1ZXN0Gh4uYXV0aC52MS5Mb2dpbkJ5R29vZ2xlUmVzcG9uc2UiKILT5JMCIjoBKiIdL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9nb29nbGUSgAEKD0xvZ2luQnlPdmVybGVhZhIfLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVxdWVzdBogLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVzcG9uc2UiKoLT5JMCJDoBKiIfL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9vdmVybGVhZhKPAQoMUmVmcmVzaFRva2VuEhwuYXV0aC52MS5SZWZyZXNoVG9rZW5SZXF1ZXN0Gh0uYXV0aC52MS5SZWZyZXNoVG9rZW5SZXNwb25zZSJCgtPkkwI8OgEqWh06ASoiGC9fcGQvYXBpL3YyL2F1dGgvcmVmcmVzaCIYL19wZC9hcGkvdjEvYXV0aC9yZWZyZXNoEl0KBkxvZ291dBIWLmF1dGgudjEuTG9nb3V0UmVxdWVzdBoXLmF1dGgudjEuTG9nb3V0UmVzcG9uc2UiIoLT5JMCHDoBKiIXL19wZC9hcGkvdjEvYXV0aC9sb2dvdXRCfwoLY29tLmF1dGgudjFCCUF1dGhQcm90b1ABWihwYXBlcmRlYnVnZ2VyL3BrZy9nZW4vYXBpL2F1dGgvdjE7YXV0aHYxogIDQVhYqgIHQXV0aC5WMcoCB0F1dGhcVjHiAhNBdXRoXFYxXEdQQk1ldGFkYXRh6gIIQXV0aDo6VjFiBnByb3RvMw", [file_google_api_annotations]);

/**
* @generated from message auth.v1.LoginByGoogleRequest
Expand Down
7 changes: 0 additions & 7 deletions webapp/_webapp/src/query/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
LoginByOverleafResponseSchema,
LogoutRequest,
LogoutResponseSchema,
RefreshTokenRequest,
RefreshTokenResponseSchema,
} from "../pkg/gen/apiclient/auth/v1/auth_pb";
import {
CreateConversationMessageStreamRequest,
Expand Down Expand Up @@ -71,11 +69,6 @@ export const loginByGoogle = async (data: PlainMessage<LoginByGoogleRequest>) =>
return fromJson(LoginByGoogleResponseSchema, response);
};

export const refreshToken = async (data: PlainMessage<RefreshTokenRequest>) => {
const response = await apiclient.post("/auth/refresh", data);
return fromJson(RefreshTokenResponseSchema, response);
};

export const logout = async (data: PlainMessage<LogoutRequest>) => {
const response = await apiclient.post("/auth/logout", data, {
ignoreErrorToast: true,
Expand Down