diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..8557c2c --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,23 @@ +name: golangci-lint +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.0 diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..2cacf70 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,15 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/jackrosenthal/algobowl/gen +plugins: + - remote: buf.build/protocolbuffers/go:v1.36.6 + out: gen + opt: + - paths=source_relative + - remote: buf.build/connectrpc/go:v1.18.1 + out: gen + opt: + - paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..a851a10 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,10 @@ +# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml +version: v2 +lint: + use: + - STANDARD +breaking: + use: + - FILE +modules: + - path: proto diff --git a/gen/algobowl/user/v1/user.pb.go b/gen/algobowl/user/v1/user.pb.go new file mode 100644 index 0000000..8da585f --- /dev/null +++ b/gen/algobowl/user/v1/user.pb.go @@ -0,0 +1,258 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: algobowl/user/v1/user.proto + +package userv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + IsAdmin bool `protobuf:"varint,5,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_algobowl_user_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_algobowl_user_v1_user_proto_msgTypes[0] + 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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_algobowl_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *User) GetIsAdmin() bool { + if x != nil { + return x.IsAdmin + } + return false +} + +type GetUserInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserInfoRequest) Reset() { + *x = GetUserInfoRequest{} + mi := &file_algobowl_user_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserInfoRequest) ProtoMessage() {} + +func (x *GetUserInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_algobowl_user_v1_user_proto_msgTypes[1] + 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 GetUserInfoRequest.ProtoReflect.Descriptor instead. +func (*GetUserInfoRequest) Descriptor() ([]byte, []int) { + return file_algobowl_user_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *GetUserInfoRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type GetUserInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserInfoResponse) Reset() { + *x = GetUserInfoResponse{} + mi := &file_algobowl_user_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserInfoResponse) ProtoMessage() {} + +func (x *GetUserInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_algobowl_user_v1_user_proto_msgTypes[2] + 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 GetUserInfoResponse.ProtoReflect.Descriptor instead. +func (*GetUserInfoResponse) Descriptor() ([]byte, []int) { + return file_algobowl_user_v1_user_proto_rawDescGZIP(), []int{2} +} + +func (x *GetUserInfoResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_algobowl_user_v1_user_proto protoreflect.FileDescriptor + +const file_algobowl_user_v1_user_proto_rawDesc = "" + + "\n" + + "\x1balgobowl/user/v1/user.proto\x12\x10algobowl.user.v1\"\x86\x01\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12!\n" + + "\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x19\n" + + "\bis_admin\x18\x05 \x01(\bR\aisAdmin\"0\n" + + "\x12GetUserInfoRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\"A\n" + + "\x13GetUserInfoResponse\x12*\n" + + "\x04user\x18\x01 \x01(\v2\x16.algobowl.user.v1.UserR\x04user2i\n" + + "\vUserService\x12Z\n" + + "\vGetUserInfo\x12$.algobowl.user.v1.GetUserInfoRequest\x1a%.algobowl.user.v1.GetUserInfoResponseB\xc2\x01\n" + + "\x14com.algobowl.user.v1B\tUserProtoP\x01Z=github.com/jackrosenthal/algobowl/gen/algobowl/user/v1;userv1\xa2\x02\x03AUX\xaa\x02\x10Algobowl.User.V1\xca\x02\x10Algobowl\\User\\V1\xe2\x02\x1cAlgobowl\\User\\V1\\GPBMetadata\xea\x02\x12Algobowl::User::V1b\x06proto3" + +var ( + file_algobowl_user_v1_user_proto_rawDescOnce sync.Once + file_algobowl_user_v1_user_proto_rawDescData []byte +) + +func file_algobowl_user_v1_user_proto_rawDescGZIP() []byte { + file_algobowl_user_v1_user_proto_rawDescOnce.Do(func() { + file_algobowl_user_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_algobowl_user_v1_user_proto_rawDesc), len(file_algobowl_user_v1_user_proto_rawDesc))) + }) + return file_algobowl_user_v1_user_proto_rawDescData +} + +var file_algobowl_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_algobowl_user_v1_user_proto_goTypes = []any{ + (*User)(nil), // 0: algobowl.user.v1.User + (*GetUserInfoRequest)(nil), // 1: algobowl.user.v1.GetUserInfoRequest + (*GetUserInfoResponse)(nil), // 2: algobowl.user.v1.GetUserInfoResponse +} +var file_algobowl_user_v1_user_proto_depIdxs = []int32{ + 0, // 0: algobowl.user.v1.GetUserInfoResponse.user:type_name -> algobowl.user.v1.User + 1, // 1: algobowl.user.v1.UserService.GetUserInfo:input_type -> algobowl.user.v1.GetUserInfoRequest + 2, // 2: algobowl.user.v1.UserService.GetUserInfo:output_type -> algobowl.user.v1.GetUserInfoResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_algobowl_user_v1_user_proto_init() } +func file_algobowl_user_v1_user_proto_init() { + if File_algobowl_user_v1_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_algobowl_user_v1_user_proto_rawDesc), len(file_algobowl_user_v1_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_algobowl_user_v1_user_proto_goTypes, + DependencyIndexes: file_algobowl_user_v1_user_proto_depIdxs, + MessageInfos: file_algobowl_user_v1_user_proto_msgTypes, + }.Build() + File_algobowl_user_v1_user_proto = out.File + file_algobowl_user_v1_user_proto_goTypes = nil + file_algobowl_user_v1_user_proto_depIdxs = nil +} diff --git a/gen/algobowl/user/v1/userv1connect/user.connect.go b/gen/algobowl/user/v1/userv1connect/user.connect.go new file mode 100644 index 0000000..ab3e164 --- /dev/null +++ b/gen/algobowl/user/v1/userv1connect/user.connect.go @@ -0,0 +1,108 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: algobowl/user/v1/user.proto + +package userv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/jackrosenthal/algobowl/gen/algobowl/user/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // UserServiceName is the fully-qualified name of the UserService service. + UserServiceName = "algobowl.user.v1.UserService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // UserServiceGetUserInfoProcedure is the fully-qualified name of the UserService's GetUserInfo RPC. + UserServiceGetUserInfoProcedure = "/algobowl.user.v1.UserService/GetUserInfo" +) + +// UserServiceClient is a client for the algobowl.user.v1.UserService service. +type UserServiceClient interface { + GetUserInfo(context.Context, *connect.Request[v1.GetUserInfoRequest]) (*connect.Response[v1.GetUserInfoResponse], error) +} + +// NewUserServiceClient constructs a client for the algobowl.user.v1.UserService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UserServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + userServiceMethods := v1.File_algobowl_user_v1_user_proto.Services().ByName("UserService").Methods() + return &userServiceClient{ + getUserInfo: connect.NewClient[v1.GetUserInfoRequest, v1.GetUserInfoResponse]( + httpClient, + baseURL+UserServiceGetUserInfoProcedure, + connect.WithSchema(userServiceMethods.ByName("GetUserInfo")), + connect.WithClientOptions(opts...), + ), + } +} + +// userServiceClient implements UserServiceClient. +type userServiceClient struct { + getUserInfo *connect.Client[v1.GetUserInfoRequest, v1.GetUserInfoResponse] +} + +// GetUserInfo calls algobowl.user.v1.UserService.GetUserInfo. +func (c *userServiceClient) GetUserInfo(ctx context.Context, req *connect.Request[v1.GetUserInfoRequest]) (*connect.Response[v1.GetUserInfoResponse], error) { + return c.getUserInfo.CallUnary(ctx, req) +} + +// UserServiceHandler is an implementation of the algobowl.user.v1.UserService service. +type UserServiceHandler interface { + GetUserInfo(context.Context, *connect.Request[v1.GetUserInfoRequest]) (*connect.Response[v1.GetUserInfoResponse], error) +} + +// NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path +// on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + userServiceMethods := v1.File_algobowl_user_v1_user_proto.Services().ByName("UserService").Methods() + userServiceGetUserInfoHandler := connect.NewUnaryHandler( + UserServiceGetUserInfoProcedure, + svc.GetUserInfo, + connect.WithSchema(userServiceMethods.ByName("GetUserInfo")), + connect.WithHandlerOptions(opts...), + ) + return "/algobowl.user.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case UserServiceGetUserInfoProcedure: + userServiceGetUserInfoHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedUserServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedUserServiceHandler struct{} + +func (UnimplementedUserServiceHandler) GetUserInfo(context.Context, *connect.Request[v1.GetUserInfoRequest]) (*connect.Response[v1.GetUserInfoResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("algobowl.user.v1.UserService.GetUserInfo is not implemented")) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3fd54e1 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/jackrosenthal/algobowl + +go 1.24.1 + +require ( + connectrpc.com/connect v1.18.1 + github.com/alecthomas/kong v1.10.0 + golang.org/x/net v0.38.0 + google.golang.org/protobuf v1.34.2 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..734fbd8 --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= +github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/proto/algobowl/user/v1/user.proto b/proto/algobowl/user/v1/user.proto new file mode 100644 index 0000000..b08700c --- /dev/null +++ b/proto/algobowl/user/v1/user.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package algobowl.user.v1; + +message User { + uint64 id = 1; + string username = 2; + string email = 3; + string display_name = 4; + bool is_admin = 5; +} + +message GetUserInfoRequest { + string username = 1; +} + +message GetUserInfoResponse { + User user = 1; +} + +service UserService { + rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse); +} diff --git a/server/api/server.go b/server/api/server.go new file mode 100644 index 0000000..994a106 --- /dev/null +++ b/server/api/server.go @@ -0,0 +1,9 @@ +package api + +import ( + "gorm.io/gorm" +) + +type ServerGlobals struct { + DbSession *gorm.DB +} diff --git a/server/api/user/user.go b/server/api/user/user.go new file mode 100644 index 0000000..017b9f7 --- /dev/null +++ b/server/api/user/user.go @@ -0,0 +1,40 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + userv1 "github.com/jackrosenthal/algobowl/gen/algobowl/user/v1" + "github.com/jackrosenthal/algobowl/server/api" + "github.com/jackrosenthal/algobowl/server/dbmodel" + "gorm.io/gorm" +) + +type UserService struct { + Globals *api.ServerGlobals +} + +func (s *UserService) GetUserInfo( + ctx context.Context, + req *connect.Request[userv1.GetUserInfoRequest], +) (*connect.Response[userv1.GetUserInfoResponse], error) { + var user dbmodel.User + result := s.Globals.DbSession.Take(&user, "username = ?", req.Msg.Username) + if result.Error == gorm.ErrRecordNotFound { + return nil, connect.NewError(connect.CodeNotFound, result.Error) + } + + if result.Error != nil { + return nil, connect.NewError(connect.CodeInternal, result.Error) + } + + return connect.NewResponse(&userv1.GetUserInfoResponse{ + User: &userv1.User{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + DisplayName: user.FullName, + IsAdmin: user.Admin, + }, + }), nil +} diff --git a/server/dbmodel/user.go b/server/dbmodel/user.go new file mode 100644 index 0000000..3dc2117 --- /dev/null +++ b/server/dbmodel/user.go @@ -0,0 +1,13 @@ +package dbmodel + +type User struct { + Id uint64 `gorm:"primaryKey"` + Username string + Email string + FullName string + Admin bool +} + +func (User) TableName() string { + return "user" +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..f758b8c --- /dev/null +++ b/server/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "log/slog" + "net/http" + + "github.com/alecthomas/kong" + "github.com/jackrosenthal/algobowl/gen/algobowl/user/v1/userv1connect" + "github.com/jackrosenthal/algobowl/server/api" + "github.com/jackrosenthal/algobowl/server/api/user" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var opts struct { + DbDriver string `env:"DB_DRIVER" enum:"sqlite,postgres" default:"sqlite" help:"Database driver"` + DbDsn string `env:"DB_DSN" default:"file:devdata.db" help:"Database connection string"` + ListenAddr string `env:"LISTEN_ADDR" default:":8080" help:"Listen address"` +} + +func connectToDb(driver string, dsn string) (*gorm.DB, error) { + var dbCon gorm.Dialector + switch driver { + case "sqlite": + dbCon = sqlite.Open(dsn) + case "postgres": + dbCon = postgres.Open(dsn) + default: + return nil, fmt.Errorf("unsupported database driver: %s", driver) + } + + db, err := gorm.Open(dbCon, &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + return db, nil +} + +func getServer(globals *api.ServerGlobals) *http.ServeMux { + mux := http.NewServeMux() + mux.Handle(userv1connect.NewUserServiceHandler(&user.UserService{Globals: globals})) + return mux +} + +func main() { + kong.Parse(&opts) + db, err := connectToDb(opts.DbDriver, opts.DbDsn) + if err != nil { + panic(fmt.Errorf("failed to connect to database: %w", err)) + } + + globals := &api.ServerGlobals{ + DbSession: db, + } + mux := getServer(globals) + + slog.Info("Server listening", "addr", opts.ListenAddr) + err = http.ListenAndServe(opts.ListenAddr, h2c.NewHandler(mux, &http2.Server{})) + panic(err) +}