feat(api,db): add /user routes

master
ALI Hamza 2021-09-30 17:32:18 +07:00
parent 071ff8ac68
commit f24d6b6bda
Signed by: hamza
GPG Key ID: 22473A32291F8CB6
20 changed files with 1255 additions and 753 deletions

1
.gitignore vendored

@ -1,5 +1,6 @@
.DS_Store
node_modules
.idea
/build
/.svelte-kit
/package

@ -0,0 +1,86 @@
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.8.2 --package=api --generate types -o types.gen.go ../schema/schema.yaml
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.8.2 --package=api --generate chi-server -o routes.gen.go ../schema/schema.yaml
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/hhhapz/hackathon/models"
"golang.org/x/oauth2"
)
// inProd tells the server whether it is in prod.
// It should be set using ldflags on build time.
var inProd bool
type Server struct {
*AuthService
*UserService
}
type OAuthStore interface {
Create(callback string) (code string)
Validate(code string) (string, bool)
Exchange(ctx context.Context, code string) (*oauth2.Token, error)
Remove(code string)
}
type UserStore interface {
CreateToken(ctx context.Context, user *models.User) (*models.Token, error)
RevokeToken(ctx context.Context, token string) error
RevokeUserTokens(ctx context.Context, user *models.User) (int64, error)
User(ctx context.Context, email string) (*models.User, error)
Users(ctx context.Context) ([]*models.User, error)
DecodeUser(ctx context.Context, buf []byte) (*models.User, error)
UserByToken(ctx context.Context, token string) (*models.User, error)
UpdateUser(ctx context.Context, user *models.User) error
}
var _ ServerInterface = (*Server)(nil)
func NewServer(oaStore OAuthStore, userStore UserStore) *Server {
return &Server{
AuthService: &AuthService{
oauthStore: oaStore,
userStore: userStore,
},
UserService: &UserService{
userStore: userStore,
},
}
}
func serverError(w http.ResponseWriter, code int, message string) {
w.WriteHeader(code)
json.NewEncoder(w).Encode(Error{
Code: code,
Message: message,
})
}
type parseError struct {
typ string
value string
reason string
message string
}
func newParseError(typ, value, reason string) error {
return parseError{
typ: typ,
value: value,
reason: reason,
message: "invalid %s(%q): %s",
}
}
func (err parseError) Error() string {
return fmt.Sprintf(err.message, err.typ, err.value, err.reason)
}

@ -3,18 +3,20 @@ package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/hhhapz/hackathon/auth"
"github.com/hhhapz/hackathon/models"
"golang.org/x/oauth2"
)
type AuthService struct {
oauthConfig auth.OAuthConfig
oauthStore OAuthStore
userStore UserStore
}
@ -25,7 +27,7 @@ func (as *AuthService) GenOauth(w http.ResponseWriter, r *http.Request, params G
serverError(w, http.StatusUnprocessableEntity, "invalid callback provided")
}
page := as.oauthConfig.AuthCodeURL(as.oauthStore, params.Callback)
page := as.oauthStore.Create(params.Callback)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
@ -35,21 +37,30 @@ func (as *AuthService) GenOauth(w http.ResponseWriter, r *http.Request, params G
var couldNotValidate = "Unable to authorize. Please try again.\nIf this issue persists, please contact an admin."
func (as *AuthService) AuthorizeCallback(w http.ResponseWriter, r *http.Request, params AuthorizeCallbackParams) {
callback, token, err := as.oauthConfig.Exchange(r.Context(), as.oauthStore, params.State, params.Code)
if err == auth.ErrInvalidState {
callback, ok := as.oauthStore.Validate(params.State)
if !ok {
serverError(w, http.StatusUnprocessableEntity, "Invalid state provided")
return
}
token, err := as.oauthStore.Exchange(r.Context(), params.Code)
if err != nil {
log.Printf("error: could not perform token: %v", err)
log.Printf("error: could not perform token exchange: %v", err)
serverError(w, http.StatusInternalServerError, couldNotValidate)
return
}
accessToken, err := as.consumeToken(r.Context(), token)
reason := "serverError"
if errors.As(err, &parseError{}) {
reason = "email"
}
if err != nil {
log.Printf("error: could not consume oauth token: %v", err)
http.Redirect(w, r, callback+"/fail", http.StatusFound)
uri := fmt.Sprintf("%s/fail?reason=%s", callback, reason)
http.Redirect(w, r, uri, http.StatusFound)
return
}
@ -58,7 +69,7 @@ func (as *AuthService) AuthorizeCallback(w http.ResponseWriter, r *http.Request,
Value: accessToken,
Path: "/",
Expires: time.Now().Add(time.Hour * 24 * 14), // 2 weeks
SameSite: http.SameSiteNoneMode, // handled via CSRF
SameSite: http.SameSiteNoneMode, // handled via CORS
Secure: inProd,
})
http.Redirect(w, r, callback+"/success", http.StatusFound)
@ -68,27 +79,48 @@ const userinfoEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo?access_t
func (as *AuthService) consumeToken(ctx context.Context, token *oauth2.Token) (string, error) {
endpoint := userinfoEndpoint + token.AccessToken
client := as.oauthConfig.Client(ctx, token)
client := http.Client{Timeout: time.Second * 5}
res, err := client.Get(endpoint)
if err != nil {
return "", fmt.Errorf("could not get userinfo: %v", err)
return "", fmt.Errorf("could not get userinfo: %w", err)
}
defer res.Body.Close()
buf, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("could not read request body: %v", err)
return "", fmt.Errorf("could not read request body: %w", err)
}
user := &models.User{
CreatedAt: models.NewTime(time.Now()),
}
if err := json.Unmarshal(buf, &user); err != nil {
return "", fmt.Errorf("could not decode userinfo json: %w", err)
}
if u, err := as.userStore.User(ctx, user.Email); err == nil {
var tk *models.Token
if tk, err = as.userStore.CreateToken(ctx, u); err != nil {
return "", fmt.Errorf("could not create user token: %w", err)
}
return tk.Token, nil
}
user, err := as.userStore.DecodeUser(ctx, buf)
student, valid := parseEmail(user.Email)
if !valid {
return "", newParseError("email", user.Email, "invalid email domain")
}
user.Teacher = !student
err = as.userStore.UpdateUser(ctx, user)
if err != nil {
return "", fmt.Errorf("could not create or get user from body: %v", err)
return "", fmt.Errorf("could not create user: %w", err)
}
tk, err := as.userStore.CreateToken(ctx, user)
if err != nil {
return "", fmt.Errorf("could not create user token: %v", err)
return "", fmt.Errorf("could not create user token: %w", err)
}
return tk.Token, nil
@ -121,3 +153,18 @@ func (as *AuthService) DeleteToken(w http.ResponseWriter, r *http.Request, param
w.WriteHeader(http.StatusNoContent)
}
const domain = "jisedu.or.id"
// parseEmail parses the provided email, and returns two bools;
// 1. if the email belongs to a student: it conforms to `\d+@[domain]`, and
// 2. if it's a valid internal email (ends with `domain`).
func parseEmail(email string) (bool, bool) {
parts := strings.Split(email, "@")
if len(parts) != 2 || parts[1] != domain {
return false, false
}
_, err := strconv.Atoi(parts[1])
return err == nil, true
}

@ -1,341 +0,0 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT.
package api
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"
)
// ConsentPage defines model for ConsentPage.
type ConsentPage struct {
Url string `json:"url"`
}
// Error defines model for Error.
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}
// DefaultResponse defines model for DefaultResponse.
type DefaultResponse Error
// AuthorizeCallbackParams defines parameters for AuthorizeCallback.
type AuthorizeCallbackParams struct {
State string `json:"state"`
Code string `json:"code"`
}
// GenOauthParams defines parameters for GenOauth.
type GenOauthParams struct {
Callback string `json:"callback"`
}
// DeleteTokenParams defines parameters for DeleteToken.
type DeleteTokenParams struct {
All *bool `json:"all,omitempty"`
// User authentication token
Token string `json:"token"`
}
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /auth/authorize)
AuthorizeCallback(w http.ResponseWriter, r *http.Request, params AuthorizeCallbackParams)
// (GET /auth/code)
GenOauth(w http.ResponseWriter, r *http.Request, params GenOauthParams)
// (DELETE /auth/token)
DeleteToken(w http.ResponseWriter, r *http.Request, params DeleteTokenParams)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
}
type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc
// AuthorizeCallback operation middleware
func (siw *ServerInterfaceWrapper) AuthorizeCallback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params AuthorizeCallbackParams
// ------------- Required query parameter "state" -------------
if paramValue := r.URL.Query().Get("state"); paramValue != "" {
} else {
http.Error(w, "Query argument state is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "state", r.URL.Query(), &params.State)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter state: %s", err), http.StatusBadRequest)
return
}
// ------------- Required query parameter "code" -------------
if paramValue := r.URL.Query().Get("code"); paramValue != "" {
} else {
http.Error(w, "Query argument code is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "code", r.URL.Query(), &params.Code)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter code: %s", err), http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthorizeCallback(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// GenOauth operation middleware
func (siw *ServerInterfaceWrapper) GenOauth(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GenOauthParams
// ------------- Required query parameter "callback" -------------
if paramValue := r.URL.Query().Get("callback"); paramValue != "" {
} else {
http.Error(w, "Query argument callback is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "callback", r.URL.Query(), &params.Callback)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter callback: %s", err), http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GenOauth(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// DeleteToken operation middleware
func (siw *ServerInterfaceWrapper) DeleteToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params DeleteTokenParams
// ------------- Optional query parameter "all" -------------
if paramValue := r.URL.Query().Get("all"); paramValue != "" {
}
err = runtime.BindQueryParameter("form", true, false, "all", r.URL.Query(), &params.All)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter all: %s", err), http.StatusBadRequest)
return
}
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.DeleteToken(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
}
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/auth/authorize", wrapper.AuthorizeCallback)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/auth/code", wrapper.GenOauth)
})
r.Group(func(r chi.Router) {
r.Delete(options.BaseURL+"/auth/token", wrapper.DeleteToken)
})
return r
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/7RVy1P7NhD+VzTbHl3bBU4+lYFOSYcODI8Tw0GRN7aILAlpDQ2Z/O+dlZ0HJC1Mh98F",
"7PU+vv2+3c0SlOu8s2gpQrWEgNE7GzG9nONM9oZuRhublLOElvhRem+0kqSdLZ6is2yLqsVO8tPPAWdQ",
"wU/FNn8xfI3F7yG4AKvVKoMaowracxKo4N7i3x4VYS0ihhcMAtlVuCC0fZFG16KPyC++J+D4MSVXPGOM",
"lq5lk5D64DwG0kMrfTD8b+ZCJwkq6IOGDGjhESqIFLRtUr6Az70OWEP1kGIeN05u+oSKYJXBAH+vhHJ1",
"KlwPrEF1UpabaG0JGwwc3mGMI8b/rp8Sbv33oXCAtjO3FkaqJAx2UhuooMPfWtm9SUvS5P4VMrCy4/gL",
"topTo2FPgVPRSjWX1DqbidZFlkJb8efkVpxeT0T0qPRsVF2ogJIdpguxSZlDBkYrHOdlrPjX5E5cjtZs",
"EANaIh+rokgY807TL2Nc7kJTMDTSZDj69lU2DQZxsYYGGbxgiAPkMv81L9ndebTSa6jgOC/zEjLwktqk",
"TSF7atMfF/RbQtZgIosVTN1Mam5/7XEmjZlKNU9JguyQMESoHpagueZzj2GxJTSSJG5sqx6FHrOdddhT",
"+nCiUfKv53nM3q/scXk0zOCuqDdY64CKBDnxilPP05RBi7JOTS3h0g2Kvl/hT5ZlWN9x1g8v/AZa8fGU",
"cDDJhilNpMMjWwaZ1ot0UKE/0F6x19eEUVsZ/53TT/r8SPFRWX7bJdy9WQfu4RVTI0YnwV7i/mYCP4p4",
"cnO0w/gYJNwn/zzZ75Lbl/iXxsAu1RvYM2kibrieOmdQ2mEvPvwk8L1ndGhpfXdoBJAqKufmGrcl1x//",
"/w4dlSf7O5RwxF4pjHHWG7MQxjUN1sL19O2CrFb/BAAA//9U4MuzmAcAAA==",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %s", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %s", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %s", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
var res = make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
var resolvePath = PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
var pathToFile = url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

@ -1,55 +0,0 @@
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.8.2 --package=api --generate types,chi-server,spec -o hackathon.gen.go ../schema/schema.yaml
package api
import (
"context"
"encoding/json"
"net/http"
"github.com/hhhapz/hackathon/auth"
"github.com/hhhapz/hackathon/models"
)
// inProd tells the server whether it is in prod.
// It should be set using ldflags on build time.
var inProd bool
type Server struct {
*AuthService
}
type OAuthStore interface {
Create(callback string) (code string)
Validate(code string) (callback string, valid bool)
Remove(code string)
}
type UserStore interface {
CreateToken(ctx context.Context, user *models.User) (*models.Token, error)
RevokeToken(ctx context.Context, token string) error
RevokeUserTokens(ctx context.Context, user *models.User) (int64, error)
DecodeUser(ctx context.Context, buf []byte) (*models.User, error)
UserByToken(ctx context.Context, token string) (*models.User, error)
}
var _ ServerInterface = (*Server)(nil)
func NewServer(oaConfig auth.OAuthConfig, oaStore OAuthStore, userStore UserStore) *Server {
return &Server{
AuthService: &AuthService{
oauthConfig: oaConfig,
oauthStore: oaStore,
userStore: userStore,
},
}
}
func serverError(w http.ResponseWriter, code int, message string) {
w.WriteHeader(code)
json.NewEncoder(w).Encode(Error{
Code: code,
Message: message,
})
}

@ -0,0 +1,450 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT.
package api
import (
"fmt"
"net/http"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/go-chi/chi/v5"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /auth/authorize)
AuthorizeCallback(w http.ResponseWriter, r *http.Request, params AuthorizeCallbackParams)
// (GET /auth/code)
GenOauth(w http.ResponseWriter, r *http.Request, params GenOauthParams)
// (DELETE /auth/token)
DeleteToken(w http.ResponseWriter, r *http.Request, params DeleteTokenParams)
// (GET /users/all)
GetAllUsers(w http.ResponseWriter, r *http.Request, params GetAllUsersParams)
// (GET /users/email)
GetUserByEmail(w http.ResponseWriter, r *http.Request, params GetUserByEmailParams)
// (PATCH /users/email)
ModifyOtherUser(w http.ResponseWriter, r *http.Request, params ModifyOtherUserParams)
// (GET /users/me)
GetMe(w http.ResponseWriter, r *http.Request, params GetMeParams)
// (PATCH /users/me)
ModifyUser(w http.ResponseWriter, r *http.Request, params ModifyUserParams)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
}
type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc
// AuthorizeCallback operation middleware
func (siw *ServerInterfaceWrapper) AuthorizeCallback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params AuthorizeCallbackParams
// ------------- Required query parameter "state" -------------
if paramValue := r.URL.Query().Get("state"); paramValue != "" {
} else {
http.Error(w, "Query argument state is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "state", r.URL.Query(), &params.State)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter state: %s", err), http.StatusBadRequest)
return
}
// ------------- Required query parameter "code" -------------
if paramValue := r.URL.Query().Get("code"); paramValue != "" {
} else {
http.Error(w, "Query argument code is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "code", r.URL.Query(), &params.Code)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter code: %s", err), http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.AuthorizeCallback(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// GenOauth operation middleware
func (siw *ServerInterfaceWrapper) GenOauth(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GenOauthParams
// ------------- Required query parameter "callback" -------------
if paramValue := r.URL.Query().Get("callback"); paramValue != "" {
} else {
http.Error(w, "Query argument callback is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "callback", r.URL.Query(), &params.Callback)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter callback: %s", err), http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GenOauth(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// DeleteToken operation middleware
func (siw *ServerInterfaceWrapper) DeleteToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params DeleteTokenParams
// ------------- Optional query parameter "all" -------------
if paramValue := r.URL.Query().Get("all"); paramValue != "" {
}
err = runtime.BindQueryParameter("form", true, false, "all", r.URL.Query(), &params.All)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter all: %s", err), http.StatusBadRequest)
return
}
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.DeleteToken(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// GetAllUsers operation middleware
func (siw *ServerInterfaceWrapper) GetAllUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GetAllUsersParams
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetAllUsers(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// GetUserByEmail operation middleware
func (siw *ServerInterfaceWrapper) GetUserByEmail(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GetUserByEmailParams
// ------------- Required query parameter "email" -------------
if paramValue := r.URL.Query().Get("email"); paramValue != "" {
} else {
http.Error(w, "Query argument email is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "email", r.URL.Query(), &params.Email)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter email: %s", err), http.StatusBadRequest)
return
}
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetUserByEmail(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// ModifyOtherUser operation middleware
func (siw *ServerInterfaceWrapper) ModifyOtherUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params ModifyOtherUserParams
// ------------- Required query parameter "email" -------------
if paramValue := r.URL.Query().Get("email"); paramValue != "" {
} else {
http.Error(w, "Query argument email is required, but not found", http.StatusBadRequest)
return
}
err = runtime.BindQueryParameter("form", true, true, "email", r.URL.Query(), &params.Email)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid format for parameter email: %s", err), http.StatusBadRequest)
return
}
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ModifyOtherUser(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// GetMe operation middleware
func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GetMeParams
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetMe(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// ModifyUser operation middleware
func (siw *ServerInterfaceWrapper) ModifyUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params ModifyUserParams
var cookie *http.Cookie
if cookie, err = r.Cookie("token"); err == nil {
var value string
err = runtime.BindStyledParameter("simple", true, "token", cookie.Value, &value)
if err != nil {
http.Error(w, "Invalid format for parameter token: %s", http.StatusBadRequest)
return
}
params.Token = value
} else {
http.Error(w, "Query argument token is required, but not found", http.StatusBadRequest)
return
}
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ModifyUser(w, r, params)
}
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler(w, r.WithContext(ctx))
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
}
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/auth/authorize", wrapper.AuthorizeCallback)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/auth/code", wrapper.GenOauth)
})
r.Group(func(r chi.Router) {
r.Delete(options.BaseURL+"/auth/token", wrapper.DeleteToken)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/users/all", wrapper.GetAllUsers)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/users/email", wrapper.GetUserByEmail)
})
r.Group(func(r chi.Router) {
r.Patch(options.BaseURL+"/users/email", wrapper.ModifyOtherUser)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/users/me", wrapper.GetMe)
})
r.Group(func(r chi.Router) {
r.Patch(options.BaseURL+"/users/me", wrapper.ModifyUser)
})
return r
}

@ -0,0 +1,115 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT.
package api
import (
"time"
openapi_types "github.com/deepmap/oapi-codegen/pkg/types"
)
// ConsentPage defines model for ConsentPage.
type ConsentPage struct {
Url string `json:"url"`
}
// Error defines model for Error.
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}
// User defines model for User.
type User struct {
Admin bool `json:"admin"`
CreatedAt time.Time `json:"created_at"`
Email openapi_types.Email `json:"email"`
// GradeLevel is only present if teacher is false.
GradeLevel *int `json:"grade_level,omitempty"`
Id string `json:"id"`
Name string `json:"name"`
Picture string `json:"picture"`
Teacher bool `json:"teacher"`
}
// DefaultResponse defines model for DefaultResponse.
type DefaultResponse Error
// AuthorizeCallbackParams defines parameters for AuthorizeCallback.
type AuthorizeCallbackParams struct {
State string `json:"state"`
Code string `json:"code"`
}
// GenOauthParams defines parameters for GenOauth.
type GenOauthParams struct {
Callback string `json:"callback"`
}
// DeleteTokenParams defines parameters for DeleteToken.
type DeleteTokenParams struct {
All *bool `json:"all,omitempty"`
// User authentication token.
Token string `json:"token"`
}
// GetAllUsersParams defines parameters for GetAllUsers.
type GetAllUsersParams struct {
// User authentication token.
Token string `json:"token"`
}
// GetUserByEmailParams defines parameters for GetUserByEmail.
type GetUserByEmailParams struct {
// User email.
Email openapi_types.Email `json:"email"`
// User authentication token.
Token string `json:"token"`
}
// ModifyOtherUserJSONBody defines parameters for ModifyOtherUser.
type ModifyOtherUserJSONBody struct {
Admin bool `json:"admin"`
GradeLevel int `json:"grade_level"`
Name string `json:"name"`
NewEmail *openapi_types.Email `json:"new_email,omitempty"`
Picture string `json:"picture"`
Teacher bool `json:"teacher"`
}
// ModifyOtherUserParams defines parameters for ModifyOtherUser.
type ModifyOtherUserParams struct {
// User email.
Email openapi_types.Email `json:"email"`
// User authentication token.
Token string `json:"token"`
}
// GetMeParams defines parameters for GetMe.
type GetMeParams struct {
// User authentication token.
Token string `json:"token"`
}
// ModifyUserJSONBody defines parameters for ModifyUser.
type ModifyUserJSONBody struct {
GradeLevel int `json:"grade_level"`
Name string `json:"name"`
}
// ModifyUserParams defines parameters for ModifyUser.
type ModifyUserParams struct {
// User authentication token.
Token string `json:"token"`
}
// ModifyOtherUserJSONRequestBody defines body for ModifyOtherUser for application/json ContentType.
type ModifyOtherUserJSONRequestBody ModifyOtherUserJSONBody
// ModifyUserJSONRequestBody defines body for ModifyUser for application/json ContentType.
type ModifyUserJSONRequestBody ModifyUserJSONBody

@ -0,0 +1,210 @@
package api
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"strings"
"github.com/deepmap/oapi-codegen/pkg/types"
"github.com/hhhapz/hackathon/models"
)
type UserService struct {
userStore UserStore
}
func (us *UserService) GetMe(w http.ResponseWriter, r *http.Request, params GetMeParams) {
u, err := us.userStore.UserByToken(r.Context(), params.Token)
if err != nil {
serverError(w, http.StatusUnauthorized, "Invalid token provided")
return
}
writeUser(w, u)
}
func (us *UserService) ModifyUser(w http.ResponseWriter, r *http.Request, params ModifyUserParams) {
u, err := us.userStore.UserByToken(r.Context(), params.Token)
if err != nil {
serverError(w, http.StatusUnauthorized, "Invalid token provided")
return
}
var body ModifyUserJSONBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
serverError(w, http.StatusBadRequest, "Invalid JSON body for ModifyUser")
return
}
u.Name = strings.TrimSpace(body.Name)
if len(u.Name) < 3 || len(u.Name) > 30 {
serverError(w, http.StatusUnprocessableEntity, "Name must be between 3 and 30 characters long")
return
}
if !u.Teacher {
if body.GradeLevel < 9 || body.GradeLevel > 12 {
serverError(w, http.StatusUnprocessableEntity, "Grade Level must be between 9 and 12")
return
}
u.GradeLevel = sql.NullInt64{Int64: int64(body.GradeLevel), Valid: true}
}
err = us.userStore.UpdateUser(r.Context(), u)
if err != nil {
log.Printf("Could not modify user %s: %v", u.Email, err)
serverError(w, http.StatusInternalServerError, "Could not update user")
return
}
writeUser(w, u)
}
func (us *UserService) GetUserByEmail(w http.ResponseWriter, r *http.Request, params GetUserByEmailParams) {
u, err := us.userStore.UserByToken(r.Context(), params.Token)
if err != nil {
serverError(w, http.StatusUnauthorized, "Invalid token provided")
return
}
email := string(params.Email)
if u.Email == email {
writeUser(w, u)
return
}
if !u.Admin {
log.Printf("user accessing unauthorized endpoint: %s", u.Email)
serverError(w, http.StatusForbidden, "You are not authorized to do this")
return
}
user, err := us.userStore.User(r.Context(), email)
if err != nil {
log.Printf("Could not fetch user(%q): %v", email, err)
serverError(w, http.StatusNotFound, "Could not find user with the specified email")
return
}
writeUser(w, user)
}
func (us *UserService) ModifyOtherUser(w http.ResponseWriter, r *http.Request, params ModifyOtherUserParams) {
u, err := us.userStore.UserByToken(r.Context(), params.Token)
if err != nil {
serverError(w, http.StatusUnauthorized, "Invalid token provided")
return
}
if !u.Admin {
log.Printf("user accessing unauthorized endpoint: %s", u.Email)
serverError(w, http.StatusForbidden, "You are not authorized to do this")
return
}
user, err := us.userStore.User(r.Context(), string(params.Email))
if err != nil {
log.Printf("Could not fetch user(%q): %v", params.Email, err)
serverError(w, http.StatusNotFound, "Could not find user with the specified email")
return
}
var body ModifyOtherUserJSONBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
serverError(w, http.StatusBadRequest, "Invalid JSON body for ModifyOtherUser")
return
}
user.Name = strings.TrimSpace(body.Name)
user.Email = string(*body.NewEmail)
user.Picture = body.Picture
user.Teacher = body.Teacher
user.Admin = body.Admin
if len(user.Name) < 3 || len(user.Name) > 30 {
serverError(w, http.StatusUnprocessableEntity, "Name must be between 3 and 30 characters long")
return
}
user.GradeLevel = sql.NullInt64{}
if !user.Teacher {
if body.GradeLevel < 9 || body.GradeLevel > 12 {
serverError(w, http.StatusUnprocessableEntity, "Grade Level must be between 9 and 12")
return
}
user.GradeLevel = sql.NullInt64{Int64: int64(body.GradeLevel), Valid: true}
}
writeUser(w, user)
}
func (us *UserService) GetAllUsers(w http.ResponseWriter, r *http.Request, params GetAllUsersParams) {
u, err := us.userStore.UserByToken(r.Context(), params.Token)
if err != nil {
serverError(w, http.StatusUnauthorized, "Invalid token provided")
return
}
if !u.Admin {
log.Printf("user accessing unauthorized endpoint: %s", u.Email)
serverError(w, http.StatusForbidden, "You are not authorized to do this")
return
}
users, err := us.userStore.Users(r.Context())
if err != nil {
log.Printf("user accessing unauthorized endpoint: %s", u.Email)
serverError(w, http.StatusInternalServerError, "Could not fetch users")
return
}
writeUsers(w, users)
}
func writeUser(w http.ResponseWriter, u *models.User) {
var grade *int
if u.GradeLevel.Valid {
g := int(u.GradeLevel.Int64)
grade = &g
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{
Admin: u.Admin,
CreatedAt: u.CreatedAt.Time(),
Email: types.Email(u.Email),
GradeLevel: grade,
Id: u.ID,
Name: u.Name,
Picture: u.Picture,
Teacher: u.Teacher,
})
}
func writeUsers(w http.ResponseWriter, us []*models.User) {
var users []User
for _, u := range us {
var grade *int
if u.GradeLevel.Valid {
g := int(u.GradeLevel.Int64)
grade = &g
}
users = append(users, User{
Admin: u.Admin,
CreatedAt: u.CreatedAt.Time(),
Email: types.Email(u.Email),
GradeLevel: grade,
Id: u.ID,
Name: u.Name,
Picture: u.Picture,
Teacher: u.Teacher,
})
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}

@ -1,62 +0,0 @@
package auth
import (
"context"
"errors"
"fmt"
"os"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
type OAuthConfig struct {
*oauth2.Config
}
const (
base = "https://www.googleapis.com"
scopeEmail = base + "/auth/userinfo.email"
scopeProfile = base + "/auth/userinfo.profile"
)
func NewOauthConfig(path string) (OAuthConfig, error) {
key, err := os.ReadFile(path)
if err != nil {
return OAuthConfig{}, fmt.Errorf("could not open file: %w", err)
}
config, err := google.ConfigFromJSON(key, scopeEmail, scopeProfile)
if err != nil {
return OAuthConfig{}, fmt.Errorf("could not load config: %w", err)
}
return OAuthConfig{
Config: config,
}, nil
}
var ErrInvalidState = errors.New("invalid ouath state provided")
type OAuthManager interface {
Create(callback string) (code string)
Validate(code string) (callback string, valid bool)
}
func (cfg OAuthConfig) AuthCodeURL(manager OAuthManager, callback string) string {
state := manager.Create(callback)
fmt.Printf("state: %v\n", state)
return cfg.Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
func (cfg OAuthConfig) Exchange(ctx context.Context, manager OAuthManager, state, code string) (string, *oauth2.Token, error) {
var ok bool
var callback string
if callback, ok = manager.Validate(state); !ok {
return "", nil, ErrInvalidState
}
tk, err := cfg.Config.Exchange(ctx, code, oauth2.AccessTypeOffline)
return callback, tk, err
}

@ -11,7 +11,6 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/hhhapz/hackathon/api"
"github.com/hhhapz/hackathon/auth"
"github.com/hhhapz/hackathon/db"
"github.com/peterbourgon/ff/v3"
)
@ -34,13 +33,12 @@ func run() error {
return fmt.Errorf("could not open db: %v", err)
}
oaStore := db.NewOAuthState()
oaConfig, err := auth.NewOauthConfig(*secretFile)
oaStore, err := db.NewOAuthState(*secretFile)
if err != nil {
return fmt.Errorf("could not create oauth config: %w", err)
}
server := api.NewServer(oaConfig, oaStore, database)
server := api.NewServer(oaStore, database)
r := chi.NewRouter()
r.Use(middleware.Logger)

@ -1,25 +1,49 @@
package db
import (
"context"
"fmt"
"os"
"sync"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
base = "https://www.googleapis.com"
scopeEmail = base + "/auth/userinfo.email"
scopeProfile = base + "/auth/userinfo.profile"
)
type OAuthState struct {
*oauth2.Config
states map[string]oauthEntry
m sync.Mutex
}
type oauthEntry struct {
created time.Time
callback string
}
type OAuthState struct {
states map[string]oauthEntry
m sync.Mutex
func NewOAuthState(path string) (*OAuthState, error) {
key, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not open file: %w", err)
}
config, err := google.ConfigFromJSON(key, scopeEmail, scopeProfile)
if err != nil {
return nil, fmt.Errorf("could not load config: %w", err)
}
func NewOAuthState() *OAuthState {
return &OAuthState{
Config: config,
states: make(map[string]oauthEntry),
m: sync.Mutex{},
}
}, nil
}
func (o *OAuthState) Create(callback string) string {
@ -38,15 +62,23 @@ func (o *OAuthState) Create(callback string) string {
created: time.Now(),
callback: callback,
}
return state
return o.Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
func (o *OAuthState) Validate(code string) (string, bool) {
func (o *OAuthState) Validate(state string) (string, bool) {
o.m.Lock()
defer o.m.Unlock()
entry, ok := o.states[code]
return entry.callback, ok
entry, ok := o.states[state]
if !ok {
return "", false
}
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
}
func (o *OAuthState) Remove(code string) {

@ -2,41 +2,22 @@ package db
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/hhhapz/hackathon/models"
)
func (db *DB) DecodeUser(ctx context.Context, buf []byte) (*models.User, error) {
user := &models.User{
Completed: false,
CreatedAt: models.NewTime(time.Now()),
}
if err := json.Unmarshal(buf, &user); err != nil {
return nil, fmt.Errorf("could not decode userinfo json: %v", err)
}
if user.ID == "" {
return nil, errors.New("no id found in userinfo")
func (db *DB) Users(ctx context.Context) ([]*models.User, error) {
return models.Users(ctx, db.DB)
}
dbUser, err := models.UserByID(ctx, db.DB, user.ID)
switch err {
case sql.ErrNoRows:
err = user.Insert(ctx, db.DB)
if err != nil {
return nil, fmt.Errorf("could not create user: %v", err)
func (db *DB) User(ctx context.Context, email string) (*models.User, error) {
return models.UserByEmail(ctx, db.DB, email)
}
dbUser = user
fallthrough
case nil:
return dbUser, nil
default:
return nil, fmt.Errorf("could not check for user %#v: %v", user, err)
func (db *DB) UpdateUser(ctx context.Context, user *models.User) error {
if err := user.Upsert(ctx, db.DB); err != nil {
return fmt.Errorf("could not upsert user: %w", err)
}
return nil
}

@ -31,6 +31,11 @@ set -ex
xo schema $DB -o $DEST
xo schema $DB --template yaml -o $DEST
# get all users
xo query $DB -M -B -T User -2 -a -F Users -o $DEST << ENDSQL
SELECT u.* FROM users u
ENDSQL
# get user by token
xo query $DB -M -B -T User -1 -2 -a -F UserByToken -o $DEST << ENDSQL
SELECT

@ -3,7 +3,7 @@ module github.com/hhhapz/hackathon
go 1.17
require (
github.com/deepmap/oapi-codegen v1.7.1
github.com/deepmap/oapi-codegen v1.8.2
github.com/getkin/kin-openapi v0.61.0
github.com/go-chi/chi/v5 v5.0.4
github.com/go-chi/cors v1.2.0

@ -63,6 +63,8 @@ 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/deepmap/oapi-codegen v1.7.1 h1:fBWh5YlHQbH0lCY+CPVlEV1fmqqPU0kBBVN71QiOyCI=
github.com/deepmap/oapi-codegen v1.7.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

@ -4,6 +4,7 @@ package models
import (
"context"
"database/sql"
)
// User represents a row from 'users'.
@ -12,7 +13,9 @@ type User struct {
Name string `json:"name"` // name
Email string `json:"email"` // email
Picture string `json:"picture"` // picture
Completed bool `json:"completed"` // completed
GradeLevel sql.NullInt64 `json:"grade_level"` // grade_level
Teacher bool `json:"teacher"` // teacher
Admin bool `json:"admin"` // admin
CreatedAt Time `json:"created_at"` // created_at
// xo fields
_exists, _deleted bool
@ -39,13 +42,13 @@ func (u *User) Insert(ctx context.Context, db DB) error {
}
// insert (manual)
const sqlstr = `INSERT INTO users (` +
`id, name, email, picture, completed, created_at` +
`id, name, email, picture, grade_level, teacher, admin, created_at` +
`) VALUES (` +
`$1, $2, $3, $4, $5, $6` +
`$1, $2, $3, $4, $5, $6, $7, $8` +
`)`
// run
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt)
if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt); err != nil {
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt)
if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt); err != nil {
return logerror(err)
}
// set exists
@ -63,11 +66,11 @@ func (u *User) Update(ctx context.Context, db DB) error {
}
// update with primary key
const sqlstr = `UPDATE users SET ` +
`name = $1, email = $2, picture = $3, completed = $4, created_at = $5 ` +
`WHERE id = $6`
`name = $1, email = $2, picture = $3, grade_level = $4, teacher = $5, admin = $6, created_at = $7 ` +
`WHERE id = $8`
// run
logf(sqlstr, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt, u.ID)
if _, err := db.ExecContext(ctx, sqlstr, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt, u.ID); err != nil {
logf(sqlstr, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt, u.ID)
if _, err := db.ExecContext(ctx, sqlstr, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt, u.ID); err != nil {
return logerror(err)
}
return nil
@ -89,16 +92,16 @@ func (u *User) Upsert(ctx context.Context, db DB) error {
}
// upsert
const sqlstr = `INSERT INTO users (` +
`id, name, email, picture, completed, created_at` +
`id, name, email, picture, grade_level, teacher, admin, created_at` +
`) VALUES (` +
`$1, $2, $3, $4, $5, $6` +
`$1, $2, $3, $4, $5, $6, $7, $8` +
`)` +
` ON CONFLICT (id) DO ` +
`UPDATE SET ` +
`name = EXCLUDED.name, email = EXCLUDED.email, picture = EXCLUDED.picture, completed = EXCLUDED.completed, created_at = EXCLUDED.created_at `
`name = EXCLUDED.name, email = EXCLUDED.email, picture = EXCLUDED.picture, grade_level = EXCLUDED.grade_level, teacher = EXCLUDED.teacher, admin = EXCLUDED.admin, created_at = EXCLUDED.created_at `
// run
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt)
if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt); err != nil {
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt)
if _, err := db.ExecContext(ctx, sqlstr, u.ID, u.Name, u.Email, u.Picture, u.GradeLevel, u.Teacher, u.Admin, u.CreatedAt); err != nil {
return logerror(err)
}
// set exists
@ -133,7 +136,7 @@ func (u *User) Delete(ctx context.Context, db DB) error {
func UserByID(ctx context.Context, db DB, id string) (*User, error) {
// query
const sqlstr = `SELECT ` +
`id, name, email, picture, completed, created_at ` +
`id, name, email, picture, grade_level, teacher, admin, created_at ` +
`FROM users ` +
`WHERE id = $1`
// run
@ -141,7 +144,7 @@ func UserByID(ctx context.Context, db DB, id string) (*User, error) {
u := User{
_exists: true,
}
if err := db.QueryRowContext(ctx, sqlstr, id).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Completed, &u.CreatedAt); err != nil {
if err := db.QueryRowContext(ctx, sqlstr, id).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil {
return nil, logerror(err)
}
return &u, nil
@ -153,7 +156,7 @@ func UserByID(ctx context.Context, db DB, id string) (*User, error) {
func UserByEmail(ctx context.Context, db DB, email string) (*User, error) {
// query
const sqlstr = `SELECT ` +
`id, name, email, picture, completed, created_at ` +
`id, name, email, picture, grade_level, teacher, admin, created_at ` +
`FROM users ` +
`WHERE email = $1`
// run
@ -161,12 +164,39 @@ func UserByEmail(ctx context.Context, db DB, email string) (*User, error) {
u := User{
_exists: true,
}
if err := db.QueryRowContext(ctx, sqlstr, email).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Completed, &u.CreatedAt); err != nil {
if err := db.QueryRowContext(ctx, sqlstr, email).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil {
return nil, logerror(err)
}
return &u, nil
}
// Users runs a custom query, returning results as User.
func Users(ctx context.Context, db DB) ([]*User, error) {
// query
const sqlstr = `SELECT u.* FROM users u`
// run
logf(sqlstr)
rows, err := db.QueryContext(ctx, sqlstr)
if err != nil {
return nil, logerror(err)
}
defer rows.Close()
// load results
var res []*User
for rows.Next() {
var u User
// scan
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil {
return nil, logerror(err)
}
res = append(res, &u)
}
if err := rows.Err(); err != nil {
return nil, logerror(err)
}
return res, nil
}
// UserByToken runs a custom query, returning results as User.
func UserByToken(ctx context.Context, db DB, token string) (*User, error) {
// query
@ -178,7 +208,7 @@ func UserByToken(ctx context.Context, db DB, token string) (*User, error) {
// run
logf(sqlstr, token)
var u User
if err := db.QueryRowContext(ctx, sqlstr, token).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.Completed, &u.CreatedAt); err != nil {
if err := db.QueryRowContext(ctx, sqlstr, token).Scan(&u.ID, &u.Name, &u.Email, &u.Picture, &u.GradeLevel, &u.Teacher, &u.Admin, &u.CreatedAt); err != nil {
return nil, logerror(err)
}
return &u, nil

@ -1,153 +0,0 @@
package models
// Code generated by xo. DO NOT EDIT.
import (
"context"
)
// UserInfo represents a row from 'user_info'.
type UserInfo struct {
UserID string `json:"user_id"` // user_id
GradeLevel int `json:"grade_level"` // grade_level
Teacher bool `json:"teacher"` // teacher
Admin bool `json:"admin"` // admin
// xo fields
_exists, _deleted bool
}
// Exists returns true when the UserInfo exists in the database.
func (ui *UserInfo) Exists() bool {
return ui._exists
}
// Deleted returns true when the UserInfo has been marked for deletion from
// the database.
func (ui *UserInfo) Deleted() bool {
return ui._deleted
}
// Insert inserts the UserInfo to the database.
func (ui *UserInfo) Insert(ctx context.Context, db DB) error {
switch {
case ui._exists: // already exists
return logerror(&ErrInsertFailed{ErrAlreadyExists})
case ui._deleted: // deleted
return logerror(&ErrInsertFailed{ErrMarkedForDeletion})
}
// insert (manual)
const sqlstr = `INSERT INTO user_info (` +
`user_id, grade_level, teacher, admin` +
`) VALUES (` +
`$1, $2, $3, $4` +
`)`
// run
logf(sqlstr, ui.UserID, ui.GradeLevel, ui.Teacher, ui.Admin)
if _, err := db.ExecContext(ctx, sqlstr, ui.UserID, ui.GradeLevel, ui.Teacher, ui.Admin); err != nil {
return logerror(err)
}
// set exists
ui._exists = true
return nil
}
// Update updates a UserInfo in the database.
func (ui *UserInfo) Update(ctx context.Context, db DB) error {
switch {
case !ui._exists: // doesn't exist
return logerror(&ErrUpdateFailed{ErrDoesNotExist})
case ui._deleted: // deleted
return logerror(&ErrUpdateFailed{ErrMarkedForDeletion})
}
// update with primary key
const sqlstr = `UPDATE user_info SET ` +
`grade_level = $1, teacher = $2, admin = $3 ` +
`WHERE user_id = $4`
// run
logf(sqlstr, ui.GradeLevel, ui.Teacher, ui.Admin, ui.UserID)
if _, err := db.ExecContext(ctx, sqlstr, ui.GradeLevel, ui.Teacher, ui.Admin, ui.UserID); err != nil {
return logerror(err)
}
return nil
}
// Save saves the UserInfo to the database.
func (ui *UserInfo) Save(ctx context.Context, db DB) error {
if ui.Exists() {
return ui.Update(ctx, db)
}
return ui.Insert(ctx, db)
}
// Upsert performs an upsert for UserInfo.
func (ui *UserInfo) Upsert(ctx context.Context, db DB) error {
switch {
case ui._deleted: // deleted
return logerror(&ErrUpsertFailed{ErrMarkedForDeletion})
}
// upsert
const sqlstr = `INSERT INTO user_info (` +
`user_id, grade_level, teacher, admin` +
`) VALUES (` +
`$1, $2, $3, $4` +
`)` +
` ON CONFLICT (user_id) DO ` +
`UPDATE SET ` +
`grade_level = EXCLUDED.grade_level, teacher = EXCLUDED.teacher, admin = EXCLUDED.admin `
// run
logf(sqlstr, ui.UserID, ui.GradeLevel, ui.Teacher, ui.Admin)
if _, err := db.ExecContext(ctx, sqlstr, ui.UserID, ui.GradeLevel, ui.Teacher, ui.Admin); err != nil {
return logerror(err)
}
// set exists
ui._exists = true
return nil
}
// Delete deletes the UserInfo from the database.
func (ui *UserInfo) Delete(ctx context.Context, db DB) error {
switch {
case !ui._exists: // doesn't exist
return nil
case ui._deleted: // deleted
return nil
}
// delete with single primary key
const sqlstr = `DELETE FROM user_info ` +
`WHERE user_id = $1`
// run
logf(sqlstr, ui.UserID)
if _, err := db.ExecContext(ctx, sqlstr, ui.UserID); err != nil {
return logerror(err)
}
// set deleted
ui._deleted = true
return nil
}
// UserInfoByUserID retrieves a row from 'user_info' as a UserInfo.
//
// Generated from index 'sqlite_autoindex_user_info_1'.
func UserInfoByUserID(ctx context.Context, db DB, userID string) (*UserInfo, error) {
// query
const sqlstr = `SELECT ` +
`user_id, grade_level, teacher, admin ` +
`FROM user_info ` +
`WHERE user_id = $1`
// run
logf(sqlstr, userID)
ui := UserInfo{
_exists: true,
}
if err := db.QueryRowContext(ctx, sqlstr, userID).Scan(&ui.UserID, &ui.GradeLevel, &ui.Teacher, &ui.Admin); err != nil {
return nil, logerror(err)
}
return &ui, nil
}
// User returns the User associated with the UserInfo's (UserID).
//
// Generated from foreign key 'user_info_user_id_fkey'.
func (ui *UserInfo) User(ctx context.Context, db DB) (*User, error) {
return UserByID(ctx, db, ui.UserID)
}

@ -60,50 +60,6 @@ schemas:
datatype:
type: text
is_primary: true
- type: table
name: user_info
columns:
- name: user_id
datatype:
type: text
is_primary: true
- name: grade_level
datatype:
type: integer
- name: teacher
datatype:
type: boolean
- name: admin
datatype:
type: boolean
primary_keys:
- name: user_id
datatype:
type: text
is_primary: true
indexes:
- name: sqlite_autoindex_user_info_1
fields:
- name: user_id
datatype:
type: text
is_primary: true
is_unique: true
is_primary: true
foreign_keys:
- name: user_info_user_id_fkey
column:
- name: user_id
datatype:
type: text
is_primary: true
ref_table: users
ref_column:
- name: id
datatype:
type: text
is_primary: true
manual: true
- type: table
name: users
columns:
@ -120,7 +76,14 @@ schemas:
- name: picture
datatype:
type: text
- name: completed
- name: grade_level
datatype:
type: integer
nullable: true
- name: teacher
datatype:
type: boolean
- name: admin
datatype:
type: boolean
- name: created_at

@ -13,6 +13,7 @@ info:
paths:
/auth/code:
get:
description: Generate oauth exchange url.
tags: ["Auth"]
operationId: gen oauth
parameters:
@ -24,7 +25,7 @@ paths:
format: uri
responses:
'200':
description: OAuth Consent Page URI
description: OAuth Consent Page URI.
content:
application/json:
schema:
@ -34,6 +35,7 @@ paths:
/auth/authorize:
get:
description: Authorization response callback location.
tags: ["Auth"]
operationId: authorize callback
parameters:
@ -49,7 +51,7 @@ paths:
type: string
responses:
'302':
description: Redirect to webpage
description: Redirect to webpage.
headers:
Location:
schema:
@ -71,20 +73,185 @@ paths:
default: false
- name: token
in: cookie
description: User authentication token
description: User authentication token.
required: true
schema:
type: string
responses:
'204':
description: User successfully logged out
description: User successfully logged out.
default:
$ref: "#/components/responses/DefaultResponse"
/users/me:
get:
description: Get self user information.
tags: ["Users"]
operationId: get me
parameters:
- name: token
in: cookie
description: User authentication token.
required: true
schema:
type: string
responses:
'200':
description: User information.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
$ref: "#/components/responses/DefaultResponse"
patch:
description: Update self user.
tags: ["Users"]
operationId: modify user
parameters:
- name: token
in: cookie
description: User authentication token.
required: true
schema:
type: string
requestBody:
description: Modified user information.
content:
application/json:
schema:
type: object
required:
- name
- grade_level
properties:
name:
type: string
grade_level:
type: integer
responses:
'200':
description: New user data
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
$ref: "#/components/responses/DefaultResponse"
/users/email:
get:
description: |-
Get user info by email.
Requires admin to get user info not equal to the owner of the token.
tags: ["Users"]
operationId: get user by email
parameters:
- name: token
in: cookie
description: User authentication token.
required: true
schema:
type: string
- name: email
in: query
description: User email.
required: true
schema:
type: string
format: email
responses:
'200':
description: User information.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
$ref: "#/components/responses/DefaultResponse"
patch:
description: Update another user. Requires admin.
tags: ["Users"]
operationId: modify other user
parameters:
- name: token
in: cookie
description: User authentication token.
required: true
schema:
type: string
- name: email
in: query
description: User email.
required: true
schema:
type: string
format: email
requestBody:
description: Modified user information.
content:
application/json:
schema:
type: object
required:
- name
- email
- picture
- grade_level
- teacher
- admin
properties:
name:
type: string
new_email:
type: string
format: email
picture:
type: string
format: uri
grade_level:
type: integer
teacher:
type: boolean
admin:
type: boolean
responses:
'200':
description: User information.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
$ref: "#/components/responses/DefaultResponse"
/users/all:
get:
description: Get all users. Requires admin.
tags: ["Users"]
operationId: get all users
parameters:
- name: token
in: cookie
description: User authentication token.
required: true
schema:
type: string
responses:
'200':
description: All user information.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
$ref: "#/components/responses/DefaultResponse"
components:
responses:
DefaultResponse:
description: Unexpected server error or invalid user input
description: Unexpected server error or invalid user input.
content:
application/json:
schema:
@ -99,6 +266,37 @@ components:
url:
type: string
format: uri
User:
type: object
required:
- id
- name
- email
- picture
- teacher
- admin
- created_at
properties:
id:
type: string
name:
type: string
email:
type: string
format: email
picture:
type: string
format: uri
grade_level:
type: integer
description: GradeLevel is only present if teacher is false.
teacher:
type: boolean
admin:
type: boolean
created_at:
type: string
format: date-time
Error:
type: object

@ -3,15 +3,10 @@ CREATE TABLE users (
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
picture TEXT NOT NULL,
completed BOOLEAN NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE user_info (
user_id TEXT NOT NULL PRIMARY KEY REFERENCES users (id),
grade_level INTEGER NOT NULL,
grade_level INTEGER,
teacher BOOLEAN NOT NULL,
admin BOOLEAN NOT NULL
admin BOOLEAN NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE tokens (