chore: add user avatar route

This commit is contained in:
Steven 2024-04-30 22:06:34 +08:00
parent cac6f42770
commit 6295979592
10 changed files with 867 additions and 380 deletions

View File

@ -76,13 +76,6 @@ var (
// The default signal sent by the `kill` command is SIGTERM,
// which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn.
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
s.Shutdown(ctx)
cancel()
}()
printGreetings()
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
@ -91,6 +84,14 @@ var (
}
}
printGreetings()
go func() {
<-c
s.Shutdown(ctx)
cancel()
}()
// Wait for CTRL-C.
<-ctx.Done()
},

View File

@ -1371,6 +1371,41 @@ paths:
type: string
tags:
- UserService
/api/v1/{name}/avatar:
get:
summary: GetUserAvatar gets the avatar of a user.
operationId: UserService_GetUserAvatar
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/apiHttpBody'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: name
description: |-
The name of the user.
Format: users/{id}
in: path
required: true
type: string
pattern: users/[^/]+
- name: httpBody.contentType
description: The HTTP Content-Type header value specifying the content type of the body.
in: query
required: false
type: string
- name: httpBody.data
description: The HTTP request/response body as raw binary.
in: query
required: false
type: string
format: byte
tags:
- UserService
/api/v1/{name}/comments:
get:
summary: ListMemoComments lists comments for a memo.
@ -1800,6 +1835,68 @@ definitions:
expiresAt:
type: string
format: date-time
apiHttpBody:
type: object
properties:
contentType:
type: string
description: The HTTP Content-Type header value specifying the content type of the body.
data:
type: string
format: byte
description: The HTTP request/response body as raw binary.
extensions:
type: array
items:
type: object
$ref: '#/definitions/protobufAny'
description: |-
Application specific response metadata. Must be set in the first response
for streaming APIs.
description: |-
Message that represents an arbitrary HTTP body. It should only be used for
payload formats that can't be represented as JSON, such as raw binary or
an HTML page.
This message can be used both in streaming and non-streaming API methods in
the request as well as the response.
It can be used as a top-level request field, which is convenient if one
wants to extract parameters from either the URL or HTTP template into the
request fields and also want access to the raw HTTP body.
Example:
message GetResourceRequest {
// A unique request id.
string request_id = 1;
// The raw HTTP body is bound to this field.
google.api.HttpBody http_body = 2;
}
service ResourceService {
rpc GetResource(GetResourceRequest)
returns (google.api.HttpBody);
rpc UpdateResource(google.api.HttpBody)
returns (google.protobuf.Empty);
}
Example with streaming methods:
service CaldavService {
rpc GetCalendar(stream google.api.HttpBody)
returns (stream google.api.HttpBody);
rpc UpdateCalendar(stream google.api.HttpBody)
returns (stream google.api.HttpBody);
}
Use of this type only changes how the request and response bodies are
handled, all other features will continue to work unchanged.
apiv1ActivityMemoCommentPayload:
type: object
properties:
@ -2016,7 +2113,122 @@ definitions:
properties:
'@type':
type: string
description: |-
A URL/resource name that uniquely identifies the type of the serialized
protocol buffer message. This string must contain at least
one "/" character. The last segment of the URL's path must represent
the fully qualified name of the type (as in
`path/google.protobuf.Duration`). The name should be in a canonical form
(e.g., leading "." is not accepted).
In practice, teams usually precompile into the binary all types that they
expect it to use in the context of Any. However, for URLs which use the
scheme `http`, `https`, or no scheme, one can optionally set up a type
server that maps type URLs to message definitions as follows:
* If no scheme is provided, `https` is assumed.
* An HTTP GET on the URL must yield a [google.protobuf.Type][]
value in binary format, or produce an error.
* Applications are allowed to cache lookup results based on the
URL, or have them precompiled into a binary to avoid any
lookup. Therefore, binary compatibility needs to be preserved
on changes to types. (Use versioned type names to manage
breaking changes.)
Note: this functionality is not currently available in the official
protobuf release, and it is not used for type URLs beginning with
type.googleapis.com. As of May 2023, there are no widely used type server
implementations and no plans to implement one.
Schemes other than `http`, `https` (or the empty scheme) might be
used with implementation specific semantics.
additionalProperties: {}
description: |-
`Any` contains an arbitrary serialized protocol buffer message along with a
URL that describes the type of the serialized message.
Protobuf library provides support to pack/unpack Any values in the form
of utility functions or additional generated methods of the Any type.
Example 1: Pack and unpack a message in C++.
Foo foo = ...;
Any any;
any.PackFrom(foo);
...
if (any.UnpackTo(&foo)) {
...
}
Example 2: Pack and unpack a message in Java.
Foo foo = ...;
Any any = Any.pack(foo);
...
if (any.is(Foo.class)) {
foo = any.unpack(Foo.class);
}
// or ...
if (any.isSameTypeAs(Foo.getDefaultInstance())) {
foo = any.unpack(Foo.getDefaultInstance());
}
Example 3: Pack and unpack a message in Python.
foo = Foo(...)
any = Any()
any.Pack(foo)
...
if any.Is(Foo.DESCRIPTOR):
any.Unpack(foo)
...
Example 4: Pack and unpack a message in Go
foo := &pb.Foo{...}
any, err := anypb.New(foo)
if err != nil {
...
}
...
foo := &pb.Foo{}
if err := any.UnmarshalTo(foo); err != nil {
...
}
The pack methods provided by protobuf library will by default use
'type.googleapis.com/full.type.name' as the type URL and the unpack
methods only use the fully qualified type name after the last '/'
in the type URL, for example "foo.bar.com/x/y.z" will yield type
name "y.z".
JSON
====
The JSON representation of an `Any` value uses the regular
representation of the deserialized, embedded message, with an
additional field `@type` which contains the type URL. Example:
package google.profile;
message Person {
string first_name = 1;
string last_name = 2;
}
{
"@type": "type.googleapis.com/google.profile.Person",
"firstName": <string>,
"lastName": <string>
}
If the embedded message type is well-known and has a custom JSON
representation, that representation will be embedded adding a field
`value` which holds the custom JSON in addition to the `@type`
field. Example (for message [google.protobuf.Duration][]):
{
"@type": "type.googleapis.com/google.protobuf.Duration",
"value": "1.212s"
}
v1Activity:
type: object
properties:

View File

@ -6,6 +6,7 @@ import "api/v1/common.proto";
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/httpbody.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
@ -26,6 +27,11 @@ service UserService {
option (google.api.http) = {get: "/api/v1/{name=users/*}"};
option (google.api.method_signature) = "name";
}
// GetUserAvatar gets the avatar of a user.
rpc GetUserAvatar(GetUserAvatarRequest) returns (google.api.HttpBody) {
option (google.api.http) = {get: "/api/v1/{name=users/*}/avatar"};
option (google.api.method_signature) = "name";
}
// CreateUser creates a new user.
rpc CreateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
@ -137,6 +143,15 @@ message GetUserRequest {
string name = 1;
}
message GetUserAvatarRequest {
// The name of the user.
// Format: users/{id}
string name = 1;
// The raw HTTP body is bound to this field.
google.api.HttpBody http_body = 2;
}
message CreateUserRequest {
User user = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -137,6 +137,76 @@ func local_request_UserService_GetUser_0(ctx context.Context, marshaler runtime.
}
var (
filter_UserService_GetUserAvatar_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetUserAvatarRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", 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_GetUserAvatar_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetUserAvatar(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetUserAvatarRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", 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_GetUserAvatar_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetUserAvatar(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CreateUserRequest
var metadata runtime.ServerMetadata
@ -732,6 +802,31 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_UserService_GetUserAvatar_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_GetUserAvatar_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_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1039,6 +1134,28 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_UserService_GetUserAvatar_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_GetUserAvatar_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_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1225,6 +1342,8 @@ var (
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_GetUserAvatar_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", "name", "avatar"}, ""))
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"}, ""))
@ -1249,6 +1368,8 @@ var (
forward_UserService_GetUser_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserAvatar_0 = runtime.ForwardResponseMessage
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage

View File

@ -8,6 +8,7 @@ package apiv1
import (
context "context"
httpbody "google.golang.org/genproto/googleapis/api/httpbody"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@ -23,6 +24,7 @@ const (
UserService_ListUsers_FullMethodName = "/memos.api.v1.UserService/ListUsers"
UserService_SearchUsers_FullMethodName = "/memos.api.v1.UserService/SearchUsers"
UserService_GetUser_FullMethodName = "/memos.api.v1.UserService/GetUser"
UserService_GetUserAvatar_FullMethodName = "/memos.api.v1.UserService/GetUserAvatar"
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
@ -43,6 +45,8 @@ type UserServiceClient interface {
SearchUsers(ctx context.Context, in *SearchUsersRequest, opts ...grpc.CallOption) (*SearchUsersResponse, error)
// GetUser gets a user by name.
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
// GetUserAvatar gets the avatar of a user.
GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
// CreateUser creates a new user.
CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
// UpdateUser updates a user.
@ -96,6 +100,15 @@ func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opt
return out, nil
}
func (c *userServiceClient) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
out := new(httpbody.HttpBody)
err := c.cc.Invoke(ctx, UserService_GetUserAvatar_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) {
out := new(User)
err := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, opts...)
@ -178,6 +191,8 @@ type UserServiceServer interface {
SearchUsers(context.Context, *SearchUsersRequest) (*SearchUsersResponse, error)
// GetUser gets a user by name.
GetUser(context.Context, *GetUserRequest) (*User, error)
// GetUserAvatar gets the avatar of a user.
GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error)
// CreateUser creates a new user.
CreateUser(context.Context, *CreateUserRequest) (*User, error)
// UpdateUser updates a user.
@ -210,6 +225,9 @@ func (UnimplementedUserServiceServer) SearchUsers(context.Context, *SearchUsersR
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
}
func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented")
}
func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented")
}
@ -301,6 +319,24 @@ func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
func _UserService_GetUserAvatar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserAvatarRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetUserAvatar(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_GetUserAvatar_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetUserAvatar(ctx, req.(*GetUserAvatarRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUserRequest)
if err := dec(in); err != nil {
@ -464,6 +500,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetUser",
Handler: _UserService_GetUser_Handler,
},
{
MethodName: "GetUserAvatar",
Handler: _UserService_GetUserAvatar_Handler,
},
{
MethodName: "CreateUser",
Handler: _UserService_CreateUser_Handler,

View File

@ -12,6 +12,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.AuthService/SignOut": true,
"/memos.api.v1.AuthService/SignUp": true,
"/memos.api.v1.UserService/GetUser": true,
"/memos.api.v1.UserService/GetUserAvatar": true,
"/memos.api.v1.UserService/SearchUsers": true,
"/memos.api.v1.MemoService/ListMemos": true,
"/memos.api.v1.MemoService/GetMemo": true,

View File

@ -2,8 +2,10 @@ package v1
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"regexp"
"slices"
"strings"
"time"
@ -14,6 +16,7 @@ import (
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
@ -100,6 +103,39 @@ func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest
return convertUserFromStore(user), nil
}
func (s *APIV1Service) GetUserAvatar(ctx context.Context, request *v1pb.GetUserAvatarRequest) (*httpbody.HttpBody, error) {
userID, err := ExtractUserIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if user.AvatarURL == "" {
return nil, status.Errorf(codes.NotFound, "avatar not found")
}
imageType, base64Data, err := extractImageInfo(user.AvatarURL)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to extract image info: %v", err)
}
imageData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to decode string: %v", err)
}
httpBody := &httpbody.HttpBody{
ContentType: imageType,
Data: imageData,
}
return httpBody, nil
}
func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
currentUser, err := getCurrentUser(ctx, s.Store)
if err != nil {
@ -567,3 +603,14 @@ func findSearchUsersField(callExpr *expr.Expr_Call, filter *SearchUsersFilter) {
}
}
}
func extractImageInfo(dataURI string) (string, string, error) {
dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
matches := dataURIRegex.FindStringSubmatch(dataURI)
if len(matches) != 3 {
return "", "", errors.New("Invalid data URI format")
}
imageType := matches[1]
base64Data := matches[2]
return imageType, base64Data, nil
}

View File

@ -40,16 +40,17 @@ func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendS
}
func (s *FrontendService) Serve(ctx context.Context, e *echo.Echo) {
skipper := func(c echo.Context) bool {
return util.HasPrefixes(c.Path(), "/api", "/memos.api.v1", "/robots.txt", "/sitemap.xml", "/m/:name")
}
// Use echo static middleware to serve the built dist folder.
// Reference: https://github.com/labstack/echo/blob/master/middleware/static.go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist"),
Skipper: func(c echo.Context) bool {
return util.HasPrefixes(c.Path(), "/api", "/memos.api.v1", "/robots.txt", "/sitemap.xml", "/m/:name")
},
Skipper: skipper,
}))
g := e.Group("assets")
// Use echo gzip middleware to compress the response.
// Reference: https://echo.labstack.com/docs/middleware/gzip
@ -68,9 +69,6 @@ func (s *FrontendService) Serve(ctx context.Context, e *echo.Echo) {
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist/assets"),
Skipper: func(c echo.Context) bool {
return util.HasPrefixes(c.Path(), "/api", "/memos.api.v1", "/robots.txt", "/sitemap.xml", "/m/:name")
},
}))
s.registerRoutes(e)

View File

@ -6,7 +6,6 @@ import (
"log/slog"
"net"
"net/http"
"strings"
"time"
"github.com/google/uuid"
@ -47,9 +46,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
echoServer.HidePort = true
s.echoServer = echoServer
// Register CORS middleware.
echoServer.Use(CORSMiddleware(s.Profile.Origins))
workspaceBasicSetting, err := s.getOrUpsertWorkspaceBasicSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace basic setting")
@ -116,15 +112,20 @@ func (s *Server) Start(ctx context.Context) error {
}
}()
go func() {
httpListener := muxServer.Match(cmux.HTTP1Fast(), cmux.Any())
httpListener := muxServer.Match(cmux.HTTP1Fast())
s.echoServer.Listener = httpListener
if err := s.echoServer.Start(address); err != nil {
slog.Error("failed to start echo server", err)
}
}()
go func() {
if err := muxServer.Serve(); err != nil {
slog.Error("mux server listen error", err)
}
}()
s.StartBackgroundRunners(ctx)
return muxServer.Serve()
return nil
}
func (s *Server) Shutdown(ctx context.Context) {
@ -170,43 +171,3 @@ func (s *Server) getOrUpsertWorkspaceBasicSetting(ctx context.Context) (*storepb
}
return workspaceBasicSetting, nil
}
func grpcRequestSkipper(c echo.Context) bool {
return strings.HasPrefix(c.Request().URL.Path, "/memos.api.v1.")
}
func CORSMiddleware(origins []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if grpcRequestSkipper(c) {
return next(c)
}
r := c.Request()
w := c.Response().Writer
requestOrigin := r.Header.Get("Origin")
if len(origins) == 0 {
w.Header().Set("Access-Control-Allow-Origin", requestOrigin)
} else {
for _, origin := range origins {
if origin == requestOrigin {
w.Header().Set("Access-Control-Allow-Origin", origin)
break
}
}
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
// If it's preflight request, return immediately.
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return nil
}
return next(c)
}
}
}