feat: add question 2, refactor api and db

master
ALI Hamza 2021-12-21 17:18:12 +07:00
parent 4bdd3b3b30
commit 87a7a9a7de
Signed by: hamza
GPG Key ID: 22473A32291F8CB6
33 changed files with 2653 additions and 441 deletions

@ -17,12 +17,11 @@ var inProd bool
type OAuthStore interface {
Create(callback string) (code string)
Validate(code string) (string, bool)
Exchange(ctx context.Context, code string) (*oauth2.Token, error)
Validate(ctx context.Context, state, code string) (*oauth2.Token, string, error)
}
type UserStore interface {
ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.Token, bool, error)
ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.Token, error)
CreateToken(ctx context.Context, user *models.User) (*models.Token, error)
RevokeToken(ctx context.Context, token string) error

@ -2,9 +2,11 @@ package api
import (
"context"
"errors"
"log"
codequestpb "github.com/hhhapz/codequest/api/v1"
"github.com/hhhapz/codequest/models"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
@ -18,8 +20,8 @@ type AuthService struct {
}
const (
MDHost = "x-forwarded-host"
UIHost = "codequest.teamortix.com"
forwardedHostKey = "x-forwarded-host"
uiHost = "codequest.teamortix.com"
)
func (as *AuthService) AuthFuncOverride(ctx context.Context, method string) (context.Context, error) {
@ -32,9 +34,9 @@ func (as *AuthService) AuthFuncOverride(ctx context.Context, method string) (con
func (as *AuthService) OAuthCode(ctx context.Context, req *codequestpb.OAuthCodeRequest) (*codequestpb.OAuthCodeResponse, error) {
md := RequestMetadata(ctx)
host := md.Get(MDHost)
host := md.Get(forwardedHostKey)
if host != UIHost && inProd {
if host != uiHost && inProd {
return nil, status.Errorf(codes.InvalidArgument, "invalid host: %v", host)
}
@ -45,19 +47,18 @@ func (as *AuthService) OAuthCode(ctx context.Context, req *codequestpb.OAuthCode
}
func (as *AuthService) Token(ctx context.Context, req *codequestpb.TokenRequest) (*codequestpb.Token, error) {
_, ok := as.oauthStore.Validate(req.State)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "invalid state %q", req.State)
}
token, err := as.oauthStore.Exchange(ctx, req.Code)
token, _, err := as.oauthStore.Validate(ctx, req.State, req.Code)
if err != nil {
if errors.As(err, &models.UserError{}) {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
log.Printf("could not perform token exchange: %v", err)
return nil, status.Errorf(codes.InvalidArgument, "invalid code %q: %v", req.Code, err)
}
tk, ok, err := as.userStore.ConsumeToken(ctx, token)
tk, err := as.userStore.ConsumeToken(ctx, token)
if err != nil {
if ok {
if errors.As(err, &models.UserError{}) {
return nil, status.Errorf(codes.InvalidArgument, "could not authorize: %v", err)
}
log.Printf("could not authorize request: %v", err)

@ -2,6 +2,7 @@ package api
import (
"context"
"errors"
"strings"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
@ -45,7 +46,10 @@ func BearerAuth(ctx context.Context, us UserStore) (context.Context, error) {
u, err := us.UserByToken(ctx, token)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", token)
if errors.As(err, &models.UserError{}) {
return nil, status.Error(codes.Unauthenticated, err.Error())
}
return nil, status.Errorf(codes.Internal, "could not authenticate your request")
}
ctx = context.WithValue(ctx, UserKey, u)

File diff suppressed because it is too large Load Diff

@ -21,6 +21,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)
// Suppress "imported and not used" errors
@ -121,6 +122,198 @@ func local_request_AuthService_DeleteToken_0(ctx context.Context, marshaler runt
}
func request_QuestService_Questions_0(ctx context.Context, marshaler runtime.Marshaler, client QuestServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.Questions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_QuestService_Questions_0(ctx context.Context, marshaler runtime.Marshaler, server QuestServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := server.Questions(ctx, &protoReq)
return msg, metadata, err
}
func request_QuestService_QuestionByID_0(ctx context.Context, marshaler runtime.Marshaler, client QuestServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QuestionByIDRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
msg, err := client.QuestionByID(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_QuestService_QuestionByID_0(ctx context.Context, marshaler runtime.Marshaler, server QuestServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QuestionByIDRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
msg, err := server.QuestionByID(ctx, &protoReq)
return msg, metadata, err
}
func request_QuestService_QuestionInput_0(ctx context.Context, marshaler runtime.Marshaler, client QuestServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QuestionInputRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
msg, err := client.QuestionInput(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_QuestService_QuestionInput_0(ctx context.Context, marshaler runtime.Marshaler, server QuestServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq QuestionInputRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
msg, err := server.QuestionInput(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_QuestService_Submit_0 = &utilities.DoubleArray{Encoding: map[string]int{"ID": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_QuestService_Submit_0(ctx context.Context, marshaler runtime.Marshaler, client QuestServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SubmitRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_QuestService_Submit_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Submit(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_QuestService_Submit_0(ctx context.Context, marshaler runtime.Marshaler, server QuestServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SubmitRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["ID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID")
}
protoReq.ID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_QuestService_Submit_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Submit(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_User_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UserRequest
var metadata runtime.ServerMetadata
@ -459,6 +652,107 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
return nil
}
// RegisterQuestServiceHandlerServer registers the http handlers for service QuestService to "mux".
// UnaryRPC :call QuestServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterQuestServiceHandlerFromEndpoint instead.
func RegisterQuestServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QuestServiceServer) error {
mux.Handle("GET", pattern_QuestService_Questions_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)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/Questions", runtime.WithHTTPPathPattern("/v1/questions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_QuestService_Questions_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_Questions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_QuestService_QuestionByID_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)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/QuestionByID", runtime.WithHTTPPathPattern("/v1/questions/{ID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_QuestService_QuestionByID_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_QuestionByID_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_QuestService_QuestionInput_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)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/QuestionInput", runtime.WithHTTPPathPattern("/v1/questions/{ID}/input"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_QuestService_QuestionInput_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_QuestionInput_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_QuestService_Submit_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)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/Submit", runtime.WithHTTPPathPattern("/v1/questions/{ID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_QuestService_Submit_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_Submit_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterUserServiceHandlerServer registers the http handlers for service UserService to "mux".
// UnaryRPC :call UserServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -723,6 +1017,147 @@ var (
forward_AuthService_DeleteToken_0 = runtime.ForwardResponseMessage
)
// RegisterQuestServiceHandlerFromEndpoint is same as RegisterQuestServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterQuestServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterQuestServiceHandler(ctx, mux, conn)
}
// RegisterQuestServiceHandler registers the http handlers for service QuestService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterQuestServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
return RegisterQuestServiceHandlerClient(ctx, mux, NewQuestServiceClient(conn))
}
// RegisterQuestServiceHandlerClient registers the http handlers for service QuestService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QuestServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QuestServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "QuestServiceClient" to call the correct interceptors.
func RegisterQuestServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QuestServiceClient) error {
mux.Handle("GET", pattern_QuestService_Questions_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)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/Questions", runtime.WithHTTPPathPattern("/v1/questions"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_QuestService_Questions_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_Questions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_QuestService_QuestionByID_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)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/QuestionByID", runtime.WithHTTPPathPattern("/v1/questions/{ID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_QuestService_QuestionByID_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_QuestionByID_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_QuestService_QuestionInput_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)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/QuestionInput", runtime.WithHTTPPathPattern("/v1/questions/{ID}/input"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_QuestService_QuestionInput_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_QuestionInput_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_QuestService_Submit_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)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/hhhapz.codequest.v1.QuestService/Submit", runtime.WithHTTPPathPattern("/v1/questions/{ID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_QuestService_Submit_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_QuestService_Submit_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_QuestService_Questions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "questions"}, ""))
pattern_QuestService_QuestionByID_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "questions", "ID"}, ""))
pattern_QuestService_QuestionInput_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"v1", "questions", "ID", "input"}, ""))
pattern_QuestService_Submit_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "questions", "ID"}, ""))
)
var (
forward_QuestService_Questions_0 = runtime.ForwardResponseMessage
forward_QuestService_QuestionByID_0 = runtime.ForwardResponseMessage
forward_QuestService_QuestionInput_0 = runtime.ForwardResponseMessage
forward_QuestService_Submit_0 = runtime.ForwardResponseMessage
)
// RegisterUserServiceHandlerFromEndpoint is same as RegisterUserServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterUserServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {

@ -173,6 +173,200 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
Metadata: "github.com/hhhapz/codequest/api/v1/all.proto",
}
// QuestServiceClient is the client API for QuestService 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 QuestServiceClient interface {
Questions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*QuestionsResponse, error)
QuestionByID(ctx context.Context, in *QuestionByIDRequest, opts ...grpc.CallOption) (*Question, error)
QuestionInput(ctx context.Context, in *QuestionInputRequest, opts ...grpc.CallOption) (*QuestionInput, error)
Submit(ctx context.Context, in *SubmitRequest, opts ...grpc.CallOption) (*SubmitResponse, error)
}
type questServiceClient struct {
cc grpc.ClientConnInterface
}
func NewQuestServiceClient(cc grpc.ClientConnInterface) QuestServiceClient {
return &questServiceClient{cc}
}
func (c *questServiceClient) Questions(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*QuestionsResponse, error) {
out := new(QuestionsResponse)
err := c.cc.Invoke(ctx, "/hhhapz.codequest.v1.QuestService/Questions", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *questServiceClient) QuestionByID(ctx context.Context, in *QuestionByIDRequest, opts ...grpc.CallOption) (*Question, error) {
out := new(Question)
err := c.cc.Invoke(ctx, "/hhhapz.codequest.v1.QuestService/QuestionByID", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *questServiceClient) QuestionInput(ctx context.Context, in *QuestionInputRequest, opts ...grpc.CallOption) (*QuestionInput, error) {
out := new(QuestionInput)
err := c.cc.Invoke(ctx, "/hhhapz.codequest.v1.QuestService/QuestionInput", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *questServiceClient) Submit(ctx context.Context, in *SubmitRequest, opts ...grpc.CallOption) (*SubmitResponse, error) {
out := new(SubmitResponse)
err := c.cc.Invoke(ctx, "/hhhapz.codequest.v1.QuestService/Submit", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// QuestServiceServer is the server API for QuestService service.
// All implementations must embed UnimplementedQuestServiceServer
// for forward compatibility
type QuestServiceServer interface {
Questions(context.Context, *emptypb.Empty) (*QuestionsResponse, error)
QuestionByID(context.Context, *QuestionByIDRequest) (*Question, error)
QuestionInput(context.Context, *QuestionInputRequest) (*QuestionInput, error)
Submit(context.Context, *SubmitRequest) (*SubmitResponse, error)
mustEmbedUnimplementedQuestServiceServer()
}
// UnimplementedQuestServiceServer must be embedded to have forward compatible implementations.
type UnimplementedQuestServiceServer struct {
}
func (UnimplementedQuestServiceServer) Questions(context.Context, *emptypb.Empty) (*QuestionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Questions not implemented")
}
func (UnimplementedQuestServiceServer) QuestionByID(context.Context, *QuestionByIDRequest) (*Question, error) {
return nil, status.Errorf(codes.Unimplemented, "method QuestionByID not implemented")
}
func (UnimplementedQuestServiceServer) QuestionInput(context.Context, *QuestionInputRequest) (*QuestionInput, error) {
return nil, status.Errorf(codes.Unimplemented, "method QuestionInput not implemented")
}
func (UnimplementedQuestServiceServer) Submit(context.Context, *SubmitRequest) (*SubmitResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Submit not implemented")
}
func (UnimplementedQuestServiceServer) mustEmbedUnimplementedQuestServiceServer() {}
// UnsafeQuestServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to QuestServiceServer will
// result in compilation errors.
type UnsafeQuestServiceServer interface {
mustEmbedUnimplementedQuestServiceServer()
}
func RegisterQuestServiceServer(s grpc.ServiceRegistrar, srv QuestServiceServer) {
s.RegisterService(&QuestService_ServiceDesc, srv)
}
func _QuestService_Questions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(QuestServiceServer).Questions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hhhapz.codequest.v1.QuestService/Questions",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(QuestServiceServer).Questions(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _QuestService_QuestionByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QuestionByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(QuestServiceServer).QuestionByID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hhhapz.codequest.v1.QuestService/QuestionByID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(QuestServiceServer).QuestionByID(ctx, req.(*QuestionByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _QuestService_QuestionInput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QuestionInputRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(QuestServiceServer).QuestionInput(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hhhapz.codequest.v1.QuestService/QuestionInput",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(QuestServiceServer).QuestionInput(ctx, req.(*QuestionInputRequest))
}
return interceptor(ctx, in, info, handler)
}
func _QuestService_Submit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(QuestServiceServer).Submit(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hhhapz.codequest.v1.QuestService/Submit",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(QuestServiceServer).Submit(ctx, req.(*SubmitRequest))
}
return interceptor(ctx, in, info, handler)
}
// QuestService_ServiceDesc is the grpc.ServiceDesc for QuestService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var QuestService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hhhapz.codequest.v1.QuestService",
HandlerType: (*QuestServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Questions",
Handler: _QuestService_Questions_Handler,
},
{
MethodName: "QuestionByID",
Handler: _QuestService_QuestionByID_Handler,
},
{
MethodName: "QuestionInput",
Handler: _QuestService_QuestionInput_Handler,
},
{
MethodName: "Submit",
Handler: _QuestService_Submit_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "github.com/hhhapz/codequest/api/v1/all.proto",
}
// UserServiceClient is the client API for UserService 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.

@ -1,7 +1,6 @@
package codequest
import (
"github.com/gunk/opt/file/java"
"github.com/gunk/opt/http"
"github.com/gunk/opt/openapiv2"
"github.com/gunk/opt/proto"

@ -2,7 +2,6 @@
package codequest
import (
"github.com/gunk/opt/file/java"
"github.com/gunk/opt/http"
"github.com/gunk/opt/openapiv2"
"github.com/gunk/opt/proto"

@ -8,6 +8,9 @@
{
"name": "AuthService"
},
{
"name": "QuestService"
},
{
"name": "UserService"
}
@ -234,6 +237,134 @@
]
}
},
"/v1/questions": {
"get": {
"operationId": "QuestService_Questions",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1QuestionsResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"QuestService"
]
}
},
"/v1/questions/{id}": {
"get": {
"operationId": "QuestService_QuestionByID",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1Question"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"QuestService"
]
},
"post": {
"operationId": "QuestService_Submit",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1SubmitResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "content",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "part",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "code",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"QuestService"
]
}
},
"/v1/questions/{id}/input": {
"get": {
"operationId": "QuestService_QuestionInput",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1QuestionInput"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"QuestService"
]
}
},
"/v1/users/me": {
"get": {
"operationId": "UserService_User",
@ -340,6 +471,87 @@
}
}
},
"v1PartData": {
"type": "object",
"properties": {
"part": {
"type": "integer",
"format": "int32"
},
"completed": {
"type": "boolean"
},
"points_worth": {
"type": "integer",
"format": "int32"
}
}
},
"v1Question": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"part1": {
"$ref": "#/definitions/v1PartData"
},
"part2": {
"$ref": "#/definitions/v1PartData"
}
}
},
"v1QuestionInput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"input": {
"type": "string"
}
}
},
"v1QuestionsResponse": {
"type": "object",
"properties": {
"questions": {
"type": "array",
"items": {
"$ref": "#/definitions/v1Question"
}
}
}
},
"v1SubmitResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"part": {
"type": "integer",
"format": "int32"
},
"correct": {
"type": "boolean"
},
"ranking": {
"type": "integer",
"format": "int32"
},
"points": {
"type": "integer",
"format": "int32"
}
}
},
"v1Token": {
"type": "object",
"properties": {
@ -358,9 +570,6 @@
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"grade_level": {
"type": "integer",
"format": "int32"

@ -39,9 +39,54 @@ export type User = {
createdAt?: GoogleProtobufTimestamp.Timestamp
}
export type PartData = {
part?: number
completed?: boolean
pointsWorth?: number
}
export type Question = {
id?: string
title?: string
content?: string
part1?: PartData
part2?: PartData
}
export type QuestionsResponse = {
questions?: Question[]
}
export type QuestionByIDRequest = {
id?: string
}
export type QuestionInputRequest = {
id?: string
}
export type QuestionInput = {
id?: string
input?: string
}
export type SubmitRequest = {
id?: string
answer?: string
part?: boolean
code?: string
}
export type SubmitResponse = {
id?: string
part?: number
correct?: boolean
ranking?: number
points?: number
}
export type UpdateFields = {
name?: string
email?: string
gradeLevel?: number
admin?: boolean
}
@ -80,6 +125,20 @@ export class AuthService {
return fm.fetchReq<DeleteTokenRequest, GoogleProtobufEmpty.Empty>(`/v1/auth/token`, {...initReq, method: "DELETE"})
}
}
export class QuestService {
static Questions(req: GoogleProtobufEmpty.Empty, initReq?: fm.InitReq): Promise<QuestionsResponse> {
return fm.fetchReq<GoogleProtobufEmpty.Empty, QuestionsResponse>(`/v1/questions?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
}
static QuestionByID(req: QuestionByIDRequest, initReq?: fm.InitReq): Promise<Question> {
return fm.fetchReq<QuestionByIDRequest, Question>(`/v1/questions/${req["id"]}?${fm.renderURLSearchParams(req, ["id"])}`, {...initReq, method: "GET"})
}
static QuestionInput(req: QuestionInputRequest, initReq?: fm.InitReq): Promise<QuestionInput> {
return fm.fetchReq<QuestionInputRequest, QuestionInput>(`/v1/questions/${req["id"]}/input?${fm.renderURLSearchParams(req, ["id"])}`, {...initReq, method: "GET"})
}
static Submit(req: SubmitRequest, initReq?: fm.InitReq): Promise<SubmitResponse> {
return fm.fetchReq<SubmitRequest, SubmitResponse>(`/v1/questions/${req["id"]}`, {...initReq, method: "POST"})
}
}
export class UserService {
static User(req: UserRequest, initReq?: fm.InitReq): Promise<User> {
return fm.fetchReq<UserRequest, User>(`/v1/users/me?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})

@ -164,13 +164,232 @@ export namespace User {
}
}
export class PartData extends jspb.Message {
getPart(): number;
setPart(value: number): void;
getCompleted(): boolean;
setCompleted(value: boolean): void;
getPointsworth(): number;
setPointsworth(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): PartData.AsObject;
static toObject(includeInstance: boolean, msg: PartData): PartData.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: PartData, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): PartData;
static deserializeBinaryFromReader(message: PartData, reader: jspb.BinaryReader): PartData;
}
export namespace PartData {
export type AsObject = {
part: number,
completed: boolean,
pointsworth: number,
}
}
export class Question extends jspb.Message {
getId(): string;
setId(value: string): void;
getTitle(): string;
setTitle(value: string): void;
getContent(): string;
setContent(value: string): void;
hasPart1(): boolean;
clearPart1(): void;
getPart1(): PartData | undefined;
setPart1(value?: PartData): void;
hasPart2(): boolean;
clearPart2(): void;
getPart2(): PartData | undefined;
setPart2(value?: PartData): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Question.AsObject;
static toObject(includeInstance: boolean, msg: Question): Question.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Question, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Question;
static deserializeBinaryFromReader(message: Question, reader: jspb.BinaryReader): Question;
}
export namespace Question {
export type AsObject = {
id: string,
title: string,
content: string,
part1?: PartData.AsObject,
part2?: PartData.AsObject,
}
}
export class QuestionsResponse extends jspb.Message {
clearQuestionsList(): void;
getQuestionsList(): Array<Question>;
setQuestionsList(value: Array<Question>): void;
addQuestions(value?: Question, index?: number): Question;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): QuestionsResponse.AsObject;
static toObject(includeInstance: boolean, msg: QuestionsResponse): QuestionsResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: QuestionsResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): QuestionsResponse;
static deserializeBinaryFromReader(message: QuestionsResponse, reader: jspb.BinaryReader): QuestionsResponse;
}
export namespace QuestionsResponse {
export type AsObject = {
questionsList: Array<Question.AsObject>,
}
}
export class QuestionByIDRequest extends jspb.Message {
getId(): string;
setId(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): QuestionByIDRequest.AsObject;
static toObject(includeInstance: boolean, msg: QuestionByIDRequest): QuestionByIDRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: QuestionByIDRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): QuestionByIDRequest;
static deserializeBinaryFromReader(message: QuestionByIDRequest, reader: jspb.BinaryReader): QuestionByIDRequest;
}
export namespace QuestionByIDRequest {
export type AsObject = {
id: string,
}
}
export class QuestionInputRequest extends jspb.Message {
getId(): string;
setId(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): QuestionInputRequest.AsObject;
static toObject(includeInstance: boolean, msg: QuestionInputRequest): QuestionInputRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: QuestionInputRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): QuestionInputRequest;
static deserializeBinaryFromReader(message: QuestionInputRequest, reader: jspb.BinaryReader): QuestionInputRequest;
}
export namespace QuestionInputRequest {
export type AsObject = {
id: string,
}
}
export class QuestionInput extends jspb.Message {
getId(): string;
setId(value: string): void;
getInput(): string;
setInput(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): QuestionInput.AsObject;
static toObject(includeInstance: boolean, msg: QuestionInput): QuestionInput.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: QuestionInput, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): QuestionInput;
static deserializeBinaryFromReader(message: QuestionInput, reader: jspb.BinaryReader): QuestionInput;
}
export namespace QuestionInput {
export type AsObject = {
id: string,
input: string,
}
}
export class SubmitRequest extends jspb.Message {
getId(): string;
setId(value: string): void;
getAnswer(): string;
setAnswer(value: string): void;
getPart(): boolean;
setPart(value: boolean): void;
getCode(): string;
setCode(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): SubmitRequest.AsObject;
static toObject(includeInstance: boolean, msg: SubmitRequest): SubmitRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: SubmitRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SubmitRequest;
static deserializeBinaryFromReader(message: SubmitRequest, reader: jspb.BinaryReader): SubmitRequest;
}
export namespace SubmitRequest {
export type AsObject = {
id: string,
answer: string,
part: boolean,
code: string,
}
}
export class SubmitResponse extends jspb.Message {
getId(): string;
setId(value: string): void;
getPart(): number;
setPart(value: number): void;
getCorrect(): boolean;
setCorrect(value: boolean): void;
getRanking(): number;
setRanking(value: number): void;
getPoints(): number;
setPoints(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): SubmitResponse.AsObject;
static toObject(includeInstance: boolean, msg: SubmitResponse): SubmitResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: SubmitResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SubmitResponse;
static deserializeBinaryFromReader(message: SubmitResponse, reader: jspb.BinaryReader): SubmitResponse;
}
export namespace SubmitResponse {
export type AsObject = {
id: string,
part: number,
correct: boolean,
ranking: number,
points: number,
}
}
export class UpdateFields extends jspb.Message {
getName(): string;
setName(value: string): void;
getEmail(): string;
setEmail(value: string): void;
getGradelevel(): number;
setGradelevel(value: number): void;
@ -190,7 +409,6 @@ export class UpdateFields extends jspb.Message {
export namespace UpdateFields {
export type AsObject = {
name: string,
email: string,
gradelevel: number,
admin: boolean,
}

@ -0,0 +1,80 @@
package codequest
import (
"github.com/gunk/opt/http"
"github.com/gunk/opt/openapiv2"
"github.com/gunk/opt/proto"
"time"
)
type QuestService interface {
// +gunk http.Match{
// Method: "GET",
// Path: "/v1/questions",
// }
Questions() QuestionsResponse
// +gunk http.Match{
// Method: "GET",
// Path: "/v1/questions/{ID}",
// }
QuestionByID(QuestionByIDRequest) Question
// +gunk http.Match{
// Method: "GET",
// Path: "/v1/questions/{ID}/input",
// }
QuestionInput(QuestionInputRequest) QuestionInput
// +gunk http.Match{
// Method: "POST",
// Path: "/v1/questions/{ID}",
// }
Submit(SubmitRequest) SubmitResponse
}
type PartData struct {
Part int `pb:"1" json:"part"`
Completed bool `pb:"2" json:"completed"`
PointsWorth int `pb:"3" json:"points_worth"`
}
type Question struct {
ID string `pb:"1" json:"id"`
Title string `pb:"2" json:"title"`
Content string `pb:"3" json:"content"`
Part1 PartData `pb:"4" json:"part1"`
Part2 PartData `pb:"5" json:"part2"`
}
type QuestionsResponse struct {
Questions []Question `pb:"1" json:"questions"`
}
type QuestionByIDRequest struct {
ID string `pb:"1" json:"id"`
}
type QuestionInputRequest struct {
ID string `pb:"1" json:"id"`
}
type QuestionInput struct {
ID string `pb:"1" json:"id"`
Input string `pb:"2" json:"input"`
}
type SubmitRequest struct {
ID string `pb:"1" json:"id"`
Answer string `pb:"2" json:"content"`
Part bool `pb:"3" json:"part"`
Code string `pb:"4" json:"code"`
}
type SubmitResponse struct {
ID string `pb:"1" json:"id"`
Part int `pb:"2" json:"part"`
Correct bool `pb:"3" json:"correct"`
Ranking int `pb:"4" json:"ranking"`
Points int `pb:"5" json:"points"`
}

@ -1,7 +1,6 @@
package codequest
import (
"github.com/gunk/opt/file/java"
"github.com/gunk/opt/http"
"github.com/gunk/opt/openapiv2"
"github.com/gunk/opt/proto"

@ -7,6 +7,7 @@ import (
"sync"
"time"
"github.com/hhhapz/codequest/models"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
@ -18,7 +19,7 @@ const (
)
type OAuthState struct {
*oauth2.Config
config *oauth2.Config
states map[string]oauthEntry
m sync.Mutex
}
@ -40,7 +41,7 @@ func NewOAuthState(path string) (*OAuthState, error) {
}
return &OAuthState{
Config: config,
config: config,
states: make(map[string]oauthEntry),
m: sync.Mutex{},
}, nil
@ -62,32 +63,28 @@ func (o *OAuthState) Create(callback string) string {
created: time.Now(),
callback: callback,
}
return o.Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
return o.config.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
func (o *OAuthState) Validate(state string) (string, bool) {
func (o *OAuthState) Validate(ctx context.Context, state string, code string) (*oauth2.Token, string, error) {
o.m.Lock()
defer o.m.Unlock()
entry, ok := o.states[state]
if !ok {
return "", false
return nil, "", models.NewUserError("invalid state: %q", state)
}
delete(o.states, state)
return entry.callback, true
}
func (o *OAuthState) Exchange(ctx context.Context, code string) (*oauth2.Token, error) {
tk, err := o.Config.Exchange(ctx, code, oauth2.AccessTypeOffline)
return tk, err
tk, err := o.config.Exchange(ctx, code, oauth2.AccessTypeOffline)
return tk, entry.callback, err
}
func (o *OAuthState) GarbageCycle(period time.Duration, duration time.Duration) {
tick := time.NewTicker(period)
clean := func() {
for range tick.C {
o.m.Lock()
defer o.m.Unlock()
now := time.Now()
for state, entry := range o.states {
@ -95,12 +92,7 @@ func (o *OAuthState) GarbageCycle(period time.Duration, duration time.Duration)
delete(o.states, state)
}
}
}
for {
select {
case <-tick.C:
clean()
}
o.m.Unlock()
}
}

@ -7,7 +7,6 @@ import (
"encoding/base64"
"errors"
"fmt"
"log"
"time"
"github.com/hhhapz/codequest/models"
@ -32,7 +31,6 @@ func (db *DB) CreateToken(ctx context.Context, user *models.User) (*models.Token
var e sqlite3.Error
if errors.As(err, &e) && e.ExtendedCode == sqlite3.ErrConstraintUnique {
log.Printf("Duplicate token string generated: %s, recreating", tokenString)
token.Token = createToken(48)
continue
}
@ -45,7 +43,10 @@ var ErrInvalidToken = errors.New("no results found")
func (db *DB) UserByToken(ctx context.Context, token string) (*models.User, error) {
user, err := models.UserByToken(ctx, db.DB, token)
if err == sql.ErrNoRows {
return nil, ErrInvalidToken
return nil, models.NewUserError("the provided token(%q) does not exist", token)
}
if err != nil {
return nil, err
}
return user, nil

@ -39,13 +39,21 @@ const (
emailDomain = "@jisedu.or.id"
)
func (db *DB) ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.Token, bool, error) {
func (db *DB) ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.Token, error) {
endpoint := userinfoEndpoint + token.AccessToken
client := http.Client{Timeout: time.Second * 5}
res, err := client.Get(endpoint)
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second*5)
defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
return nil, false, fmt.Errorf("could not get userinfo: %w", err)
return nil, fmt.Errorf("could not create request: %w", err)
}
res, err := http.DefaultClient.Do(r)
if err != nil {
return nil, fmt.Errorf("could not get userinfo: %w", err)
}
defer res.Body.Close()
@ -53,30 +61,25 @@ func (db *DB) ConsumeToken(ctx context.Context, token *oauth2.Token) (*models.To
CreatedAt: models.NewTime(time.Now()),
}
if err := json.NewDecoder(res.Body).Decode(&user); err != nil {
return nil, false, fmt.Errorf("could not decode userinfo json: %w", err)
return nil, fmt.Errorf("could not decode userinfo json: %w", err)
}
if u, err := db.User(ctx, user.Email); err == nil {
var tk *models.Token
if tk, err = db.CreateToken(ctx, u); err != nil {
return nil, false, fmt.Errorf("could not create user token: %w", err)
// register new user
if _, err := db.User(ctx, user.Email); err != nil {
if !strings.HasSuffix(user.Email, emailDomain) {
return nil, models.NewUserError("invalid registration email: %s", user.Email)
}
return tk, true, nil
}
if !strings.HasSuffix(user.Email, emailDomain) {
return nil, true, fmt.Errorf("invalid registration email %q: must end with %q", user.Email, emailDomain)
}
err = db.UpdateUser(ctx, user)
if err != nil {
return nil, false, fmt.Errorf("could not create user: %w", err)
err = db.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("could not create user: %w", err)
}
}
// gen new token
tk, err := db.CreateToken(ctx, user)
if err != nil {
return nil, false, fmt.Errorf("could not create user token: %w", err)
return nil, fmt.Errorf("could not create user token: %w", err)
}
return tk, true, nil
return tk, nil
}

@ -21,6 +21,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gunk/opt v0.2.0 // indirect
github.com/kenshaw/diskcache v0.5.1 // indirect
github.com/kenshaw/httplog v0.4.0 // indirect
github.com/kenshaw/redoc v0.1.3 // indirect

@ -177,6 +177,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 h1:rgxjzoDmDXw5q8HONgyHhBas4to0/XWRo/gPpJhsUNQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0/go.mod h1:qrJPVzv9YlhsrxJc3P/Q85nr0w1lIRikTl4JlhdDH5w=
github.com/gunk/opt v0.2.0 h1:TAfvX5vrEuMEVXs/XnaJMam9q27d9noVsXbQ4V9LAnA=
github.com/gunk/opt v0.2.0/go.mod h1:Pp/fgnNbbjanUyvaIZ+4eMPAcZNHuro8QjpVSNxwJJU=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

@ -0,0 +1,15 @@
package models
import "fmt"
type UserError struct {
reason string
}
func NewUserError(message string, args ...interface{}) UserError {
return UserError{fmt.Sprintf(message, args...)}
}
func (e UserError) Error() string {
return e.reason
}

@ -1,150 +0,0 @@
package question
import (
_ "embed"
"fmt"
"math"
"math/rand"
"strconv"
"strings"
"github.com/hhhapz/codequest/models"
)
func move(x, y int, dir byte, step int) (int, int) {
switch dir {
case 'N':
return x, y + step
case 'S':
return x, y - step
case 'E':
return x + step, y
case 'W':
return x - step, y
}
return x, y
}
func q1P1(steps []string) (x int, y int) {
for _, step := range steps {
dir := step[0]
amt, _ := strconv.Atoi(step[1:])
x, y = move(x, y, dir, amt)
}
return
}
func q1P2(steps []string) (x int, y int) {
known := make(map[int]map[int]bool)
for _, step := range steps {
dir := step[0]
amt, _ := strconv.Atoi(step[1:])
x, y = move(x, y, dir, amt)
if known[x] == nil {
known[x] = make(map[int]bool)
}
if known[x][y] {
return
}
known[x][y] = true
}
return
}
func q1Total(a, b int) int {
if a < 0 {
a = -a
}
if b < 0 {
b = -b
}
return a + b
}
const (
q1Entries = 100
q1Range = 20
directions = "NSWE"
)
func init() {
var q *Question
q = &Question{
ID: "directions",
Text: q01Text,
Level: Level1,
Generate: func(u *models.User) string {
res := make([]string, 0, 100)
r := userRandom(u)
known := make(map[int]map[int]bool)
var x, y int
for i := 0; i < q1Entries; i++ {
dir := directions[r.Intn(4)]
steps := r.Intn(30) + 1
newX, newY := move(x, y, dir, steps)
if known[newX] == nil {
known[newX] = make(map[int]bool)
}
if known[newX][newY] {
i--
continue
}
known[newX][newY] = true
x, y = newX, newY
res = append(res, fmt.Sprintf("%c%d", dir, steps))
}
n := rand.Intn(q1Range) + 10
locX, locY := q1P1(res[:n])
n = rand.Intn(q1Range) + 35
lastX, lastY := q1P1(res[:n])
dX := math.Max(float64(locX), float64(lastX)) - math.Min(float64(locX), float64(lastX))
dY := math.Max(float64(locY), float64(lastY)) - math.Min(float64(locY), float64(lastY))
if locX > lastX {
res[n] = fmt.Sprintf("E%d", int(dX))
n++
} else if locX < lastX {
res[n] = fmt.Sprintf("W%d", int(dX))
n++
}
if locY > lastY {
res[n] = fmt.Sprintf("N%d", int(dY))
} else if locY < lastY {
res[n] = fmt.Sprintf("S%d", int(dY))
}
return strings.Join(res, "\n")
},
Validate: func(u *models.User, level Part, input string) bool {
raw := q.Generate(u)
inp := strings.Split(raw, "\n")
switch level {
case Part1:
x, y := q1P1(inp)
return q1Total(x, y) == 0
case Part2:
x, y := q1P2(inp)
return q1Total(x, y) == 0
}
return false
},
}
Register(q)
}
//go:embed prompts/q01.md
var q01Text string

@ -0,0 +1,26 @@
package q01
import "strconv"
func move(x, y int, dir byte, step int) (int, int) {
switch dir {
case 'N':
return x, y + step
case 'S':
return x, y - step
case 'E':
return x + step, y
case 'W':
return x - step, y
}
return x, y
}
func solveP1(steps []string) (x int, y int) {
for _, step := range steps {
dir := step[0]
amt, _ := strconv.Atoi(step[1:])
x, y = move(x, y, dir, amt)
}
return
}

@ -0,0 +1,22 @@
package q01
import "strconv"
func solveP2(steps []string) (x int, y int) {
known := make(map[int]map[int]bool)
for _, step := range steps {
dir := step[0]
amt, _ := strconv.Atoi(step[1:])
x, y = move(x, y, dir, amt)
if known[x] == nil {
known[x] = make(map[int]bool)
}
if known[x][y] {
return
}
known[x][y] = true
}
return
}

@ -0,0 +1,131 @@
package q01
import (
_ "embed"
"fmt"
"strconv"
"strings"
"github.com/hhhapz/codequest/models"
"github.com/hhhapz/codequest/question"
)
func init() {
question.Register(
&question.Question{
ID: "directions",
Name: "No Time for Directions!",
Text: q01Text,
Level: question.Level1,
Generate: func(u *models.User) string {
return strings.Join(generate(u), "\n")
},
Validate: func(u *models.User, part question.Part, solution string) bool {
return Validate(u, part, solution)
},
})
}
func total(a, b int) int {
if a < 0 {
a = -a
}
if b < 0 {
b = -b
}
return a + b
}
const (
// totalSteps in the input
totalSteps = 100
// stepRange is the range of steps within each step
stepRange = 30
// randRage is the range of steps between where the duplicate will be
// present
randRange = 20
// earliestCandidate the earliest step where the duplicate candidate is
earliestCandidate = 10
// earliestDupe is the earliest step where the candidate is visited
earliestDupe = 35
// directions for a step
directions = "NSWE"
)
func generate(u *models.User) []string {
res := make([]string, 0, totalSteps)
r := question.UserRandom(u)
// known is used to disallow duplicate steps before the chosen dupe
known := make(map[int]map[int]bool)
var x, y int
for len(res) != cap(res) {
dir := directions[r.Intn(4)]
steps := r.Intn(stepRange) + 1
newX, newY := move(x, y, dir, steps)
if known[newX] == nil {
known[newX] = make(map[int]bool)
}
if known[newX][newY] {
continue
}
known[newX][newY] = true
x, y = newX, newY
res = append(res, fmt.Sprintf("%c%d", dir, steps))
}
// the location of the candidate for the duplicate
n := r.Intn(randRange) + earliestCandidate
locX, locY := solveP1(res[:n])
// the location where the candidate is revisited
n = r.Intn(randRange) + earliestDupe
lastX, lastY := solveP2(res[:n])
// jump to the candidate
if locX > lastX {
res[n] = fmt.Sprintf("E%d", locX-lastX)
n++
} else if locX < lastX {
res[n] = fmt.Sprintf("W%d", lastX-locX)
n++
}
if locY > lastY {
res[n] = fmt.Sprintf("N%d", locY-lastY)
} else if locY < lastY {
res[n] = fmt.Sprintf("S%d", lastY-locY)
}
return res
}
func Validate(u *models.User, p question.Part, sol string) bool {
inp := generate(u)
var t int
switch p {
case question.Part1:
t = total(solveP1(inp))
case question.Part2:
t = total(solveP2(inp))
default:
return false
}
return strconv.Itoa(t) == sol
}
//go:embed q01.md
var q01Text string

@ -1,6 +1,6 @@
# No Time For Directions!
**Hermes** is the Greek god, and amongst others, he is the god of travel, trade, and athletes.
**Hermes** is the Greek god of, amongst others, travel, trade, and athletes.
Hermes has placed his hope in you.
You're dropped to a random location near Mount Cyllene in Arcadia, widely considered the birthplace

@ -0,0 +1,54 @@
package q01
import (
"strconv"
"strings"
"testing"
"github.com/hhhapz/codequest/models"
"github.com/hhhapz/codequest/question"
)
func TestQ01(t *testing.T) {
u := &models.User{
ID: "12031209571980516",
}
q := question.QuestionByID("directions")
raw := q.Generate(u)
input := strings.Split(raw, "\n")
res := total(solveP1(input))
if !q.Validate(u, question.Part1, strconv.Itoa(res)) {
t.Errorf("Expected question 1 part 1(%v) to be correct!", res)
}
res = total(solveP2(input))
if !q.Validate(u, question.Part2, strconv.Itoa(res)) {
t.Errorf("Expected question 2 part 2(%v) to be correct!", res)
}
if q.Validate(u, question.Part1, "") {
t.Errorf("Expected bad input to be invalid")
}
if t.Failed() {
t.Logf("Input:\n%v", raw)
}
}
func TestQ01Idempotent(t *testing.T) {
q := question.QuestionByID("directions")
u1 := &models.User{ID: "1"}
raw1 := q.Generate(u1)
u2 := &models.User{ID: "2"}
_ = q.Generate(u2)
raw2 := q.Generate(u1)
if raw1 != raw2 {
t.Errorf("Expected raw1 and raw2 to be the same")
}
}

@ -1,29 +0,0 @@
package question
import (
"fmt"
"strings"
"testing"
"github.com/hhhapz/codequest/models"
)
func TestQ01(t *testing.T) {
u := &models.User{
ID: "0",
}
q := QuestionByID("directions")
input := q.Generate(u)
fmt.Println("STAAART")
x, y := q1P1(strings.Split(input, "\n"))
fmt.Println("STAAART")
t.Logf("SOLUTION (P1): (%d, %d): %d", x, y, q1Total(x, y))
x, y = q1P2(strings.Split(input, "\n"))
t.Logf("SOLUTION (P2): (%d, %d): %d", x, y, q1Total(x, y))
fmt.Printf("INPUT: %v", input)
}

@ -1,11 +0,0 @@
package question
func init() {
Register(&Question{
ID: "",
Text: "",
Level: 0,
Generate: nil,
Validate: nil,
})
}

@ -0,0 +1,21 @@
package q02
func gcd(a, b int64) int64 {
for b != 0 {
a, b = b, a%b
}
return a
}
func lcm(nums []int) int64 {
result := int64(nums[0])
for i := range nums {
n := int64(nums[i])
result = result * n / gcd(result, n)
}
return result
}
func solveP1(ships []int) int64 {
return lcm(ships)
}

@ -0,0 +1,12 @@
package q02
func solveP2(ships []int) int64 {
lcm := solveP1(ships)
var res int64
for _, v := range ships {
res += lcm / int64(v)
}
return res
}

@ -0,0 +1,87 @@
package q02
import (
_ "embed"
"math"
"math/big"
"strconv"
"strings"
"github.com/hhhapz/codequest/models"
"github.com/hhhapz/codequest/question"
)
func init() {
question.Register(
&question.Question{
ID: "saturnalia",
Name: "Saturnalia's Problem",
Text: q02Text,
Level: question.Level1,
Generate: func(u *models.User) string {
inp := generate(u)
var res []string
for _, n := range inp {
res = append(res, strconv.Itoa(n))
}
return strings.Join(res, "\n")
},
Validate: func(u *models.User, part question.Part, solution string) bool {
return Validate(u, part, solution)
},
})
}
func bigLCM(a, b *big.Int) *big.Int {
return big.NewInt(0).Div(big.NewInt(0).Mul(a, b), big.NewInt(0).GCD(nil, nil, a, b))
}
const (
ships = 25
minNum = 3
maxNum = 100
)
var maxLCM = big.NewInt(math.MaxInt64 / 100)
func generate(u *models.User) []int {
res := make([]int, 0, ships)
r := question.UserRandom(u)
lcm := big.NewInt(1)
for len(res) != cap(res) {
n := r.Intn(maxNum-minNum) + minNum
nLcm := bigLCM(lcm, big.NewInt(int64(n)))
if nLcm.Cmp(maxLCM) > 0 {
continue
}
lcm = nLcm
res = append(res, n)
}
return res
}
func Validate(u *models.User, p question.Part, sol string) bool {
inp := generate(u)
var t int64
switch p {
case question.Part1:
t = solveP1(inp)
case question.Part2:
t = solveP2(inp)
default:
return false
}
return strconv.FormatInt(t, 10) == sol
}
//go:embed q02.md
var q02Text string

@ -0,0 +1,88 @@
# Saturnalia's Problem
**Kronos** is the father of the Olympian gods, and is the king of the Titans. He is the Titan god of
Time, Justice, and Evil.
After the Olympians defeated the titans, Kronos was given a new fate: he now rules over the Isles of
the Blessed and the region of Latium near the Roman Empire. Next month, the festival **Saturnalia**.
You've been given the task to schedule and organize the arrival of the citizens of Isles of the
Blessed and Latium to reach Mount Othrys by ship.
Unfortunately, there are far too many ships to calculate this all by hand. Each ship takes a
different amount of time to return back to Mount Othrys, and you need to make sure everyone arrives
at the same time.
You have been given the shipping planner, that tells you how long it takes for each ship to reach
either Isles of the Blessed or Latium, in minutes, and return back.
Each ship initially departs at the same time, and continues to do as many rounds as needed, such
that **each ship returns back to Mount Othrys at the same time**.
Your task is to determine for the given ships, what is the fewest number of minutes until each ship
returns back at the same time after the leave?
### Example
- Given the following input
```
3
5
9
```
Says that there are 3 different ships. The first one takes `3` minutes to complete a cycle, the
second takes `5`, and `9` minutes for last ship. After `45` minutes, all of them will return back
to Mount Othrys **at the same time**.
- Given the following input
```
3
12
15
9
5
73
19
49
10
13
```
Says that there are a total of 10 ships, and all of them will return back after `159,033,420`
minutes.
**How many minutes** will your ships take to all return back at the same time?
Hint: The numbers might get a bit large here!
{{ if eq .Part 2 }}
**Congratulations! You got Part 1 correct. Your answer was `{{ .Answer1 }}`.**
## Part 2
To continue planning for the journey, Kronos needs figure out what the total number of journeys will
be for each ship, summed all together.
### Example
Given the following input:
```
3
5
9
```
The total duration will still be `45` minutes before all the ships return again to the same
position. In this time, the first ship will complete `15` trips, the second ship will complete `9`
trips, and the last ship will complete `5` trips, for a total of `29` trips.
With these new instructions, **how many steps total trips will all the ships complete?**
{{ end }}

@ -0,0 +1,58 @@
package q02
import (
"strconv"
"strings"
"testing"
"github.com/hhhapz/codequest/models"
"github.com/hhhapz/codequest/question"
)
func TestQ02(t *testing.T) {
u := &models.User{
ID: "1203120957198056",
}
q := question.QuestionByID("saturnalia")
raw := q.Generate(u)
var input []int
for _, s := range strings.Split(raw, "\n") {
i, _ := strconv.Atoi(s)
input = append(input, i)
}
res := solveP1(input)
t.Logf("part 1 result: %d", res)
if !q.Validate(u, question.Part1, strconv.FormatInt(res, 10)) {
t.Errorf("Expected question 1 part 1(%v) to be correct!", res)
}
res = solveP2(input)
if !q.Validate(u, question.Part2, strconv.FormatInt(res, 10)) {
t.Errorf("Expected question 2 part 2(%v) to be correct!", res)
}
if q.Validate(u, question.Part1, "") {
t.Errorf("Expected bad input to be invalid")
}
t.Logf("Input:\n%v", raw)
}
func TestQ02Idempotent(t *testing.T) {
q := question.QuestionByID("saturnalia")
u1 := &models.User{ID: "1"}
raw1 := q.Generate(u1)
u2 := &models.User{ID: "2"}
_ = q.Generate(u2)
raw2 := q.Generate(u1)
if raw1 != raw2 {
t.Errorf("Expected raw1 and raw2 to be the same")
}
}

@ -36,6 +36,7 @@ func Questions(user *models.User, level Level) []*Question {
type Question struct {
ID string
Name string
Text string
Level Level
@ -57,12 +58,12 @@ const (
Part2
)
func userRandom(u *models.User) *rand.Rand {
// TODO: Internal autoincrement id
func UserRandom(u *models.User) *rand.Rand {
id := u.ID
if len(id) > 17 {
id = id[:17]
}
seed, _ := strconv.ParseInt(id, 10, 64)
return rand.New(rand.NewSource(seed))
}