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 .DS_Store
node_modules node_modules
.idea
/build /build
/.svelte-kit /.svelte-kit
/package /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 ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"strconv"
"strings"
"time" "time"
"github.com/hhhapz/hackathon/auth" "github.com/hhhapz/hackathon/models"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
type AuthService struct { type AuthService struct {
oauthConfig auth.OAuthConfig
oauthStore OAuthStore oauthStore OAuthStore
userStore UserStore 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") 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.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json") 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." 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) { 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) callback, ok := as.oauthStore.Validate(params.State)
if err == auth.ErrInvalidState { if !ok {
serverError(w, http.StatusUnprocessableEntity, "Invalid state provided") serverError(w, http.StatusUnprocessableEntity, "Invalid state provided")
return return
} }
token, err := as.oauthStore.Exchange(r.Context(), params.Code)
if err != nil { 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) serverError(w, http.StatusInternalServerError, couldNotValidate)
return return
} }
accessToken, err := as.consumeToken(r.Context(), token) accessToken, err := as.consumeToken(r.Context(), token)
reason := "serverError"
if errors.As(err, &parseError{}) {
reason = "email"
}
if err != nil { if err != nil {
log.Printf("error: could not consume oauth token: %v", err) 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 return
} }
@ -58,7 +69,7 @@ func (as *AuthService) AuthorizeCallback(w http.ResponseWriter, r *http.Request,
Value: accessToken, Value: accessToken,
Path: "/", Path: "/",
Expires: time.Now().Add(time.Hour * 24 * 14), // 2 weeks Expires: time.Now().Add(time.Hour * 24 * 14), // 2 weeks
SameSite: http.SameSiteNoneMode, // handled via CSRF SameSite: http.SameSiteNoneMode, // handled via CORS
Secure: inProd, Secure: inProd,
}) })
http.Redirect(w, r, callback+"/success", http.StatusFound) 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) { func (as *AuthService) consumeToken(ctx context.Context, token *oauth2.Token) (string, error) {
endpoint := userinfoEndpoint + token.AccessToken endpoint := userinfoEndpoint + token.AccessToken
client := as.oauthConfig.Client(ctx, token) client := http.Client{Timeout: time.Second * 5}
res, err := client.Get(endpoint) res, err := client.Get(endpoint)
if err != nil { 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() defer res.Body.Close()
buf, err := io.ReadAll(res.Body) buf, err := io.ReadAll(res.Body)
if err != nil { 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 { 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) tk, err := as.userStore.CreateToken(ctx, user)
if err != nil { 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 return tk.Token, nil
@ -121,3 +153,18 @@ func (as *AuthService) DeleteToken(w http.ResponseWriter, r *http.Request, param
w.WriteHeader(http.StatusNoContent) 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/chi/v5/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/hhhapz/hackathon/api" "github.com/hhhapz/hackathon/api"
"github.com/hhhapz/hackathon/auth"
"github.com/hhhapz/hackathon/db" "github.com/hhhapz/hackathon/db"
"github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3"
) )
@ -34,13 +33,12 @@ func run() error {
return fmt.Errorf("could not open db: %v", err) return fmt.Errorf("could not open db: %v", err)
} }
oaStore := db.NewOAuthState() oaStore, err := db.NewOAuthState(*secretFile)
oaConfig, err := auth.NewOauthConfig(*secretFile)
if err != nil { if err != nil {
return fmt.Errorf("could not create oauth config: %w", err) 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 := chi.NewRouter()
r.Use(middleware.Logger) r.Use(middleware.Logger)

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

@ -2,41 +2,22 @@ package db
import ( import (
"context" "context"
"database/sql"
"encoding/json"
"errors"
"fmt" "fmt"
"time"
"github.com/hhhapz/hackathon/models" "github.com/hhhapz/hackathon/models"
) )
func (db *DB) DecodeUser(ctx context.Context, buf []byte) (*models.User, error) { func (db *DB) Users(ctx context.Context) ([]*models.User, error) {
user := &models.User{ return models.Users(ctx, db.DB)
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")
}
dbUser, err := models.UserByID(ctx, db.DB, user.ID) func (db *DB) User(ctx context.Context, email string) (*models.User, error) {
switch err { return models.UserByEmail(ctx, db.DB, email)
case sql.ErrNoRows: }
err = user.Insert(ctx, db.DB)
if err != nil {
return nil, fmt.Errorf("could not create user: %v", err)
}
dbUser = user
fallthrough
case nil:
return dbUser, nil
default: func (db *DB) UpdateUser(ctx context.Context, user *models.User) error {
return nil, fmt.Errorf("could not check for user %#v: %v", user, err) 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 -o $DEST
xo schema $DB --template yaml -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 # get user by token
xo query $DB -M -B -T User -1 -2 -a -F UserByToken -o $DEST << ENDSQL xo query $DB -M -B -T User -1 -2 -a -F UserByToken -o $DEST << ENDSQL
SELECT SELECT

@ -3,7 +3,7 @@ module github.com/hhhapz/hackathon
go 1.17 go 1.17
require ( 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/getkin/kin-openapi v0.61.0
github.com/go-chi/chi/v5 v5.0.4 github.com/go-chi/chi/v5 v5.0.4
github.com/go-chi/cors v1.2.0 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/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 h1:fBWh5YlHQbH0lCY+CPVlEV1fmqqPU0kBBVN71QiOyCI=
github.com/deepmap/oapi-codegen v1.7.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= 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/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.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= 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 ( import (
"context" "context"
"database/sql"
) )
// User represents a row from 'users'. // User represents a row from 'users'.
@ -12,7 +13,9 @@ type User struct {
Name string `json:"name"` // name Name string `json:"name"` // name
Email string `json:"email"` // email Email string `json:"email"` // email
Picture string `json:"picture"` // picture 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 CreatedAt Time `json:"created_at"` // created_at
// xo fields // xo fields
_exists, _deleted bool _exists, _deleted bool
@ -39,13 +42,13 @@ func (u *User) Insert(ctx context.Context, db DB) error {
} }
// insert (manual) // insert (manual)
const sqlstr = `INSERT INTO users (` + const sqlstr = `INSERT INTO users (` +
`id, name, email, picture, completed, created_at` + `id, name, email, picture, grade_level, teacher, admin, created_at` +
`) VALUES (` + `) VALUES (` +
`$1, $2, $3, $4, $5, $6` + `$1, $2, $3, $4, $5, $6, $7, $8` +
`)` `)`
// run // run
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt) 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.Completed, u.CreatedAt); err != nil { 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) return logerror(err)
} }
// set exists // set exists
@ -63,11 +66,11 @@ func (u *User) Update(ctx context.Context, db DB) error {
} }
// update with primary key // update with primary key
const sqlstr = `UPDATE users SET ` + const sqlstr = `UPDATE users SET ` +
`name = $1, email = $2, picture = $3, completed = $4, created_at = $5 ` + `name = $1, email = $2, picture = $3, grade_level = $4, teacher = $5, admin = $6, created_at = $7 ` +
`WHERE id = $6` `WHERE id = $8`
// run // run
logf(sqlstr, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt, u.ID) 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.Completed, u.CreatedAt, u.ID); err != nil { 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 logerror(err)
} }
return nil return nil
@ -89,16 +92,16 @@ func (u *User) Upsert(ctx context.Context, db DB) error {
} }
// upsert // upsert
const sqlstr = `INSERT INTO users (` + const sqlstr = `INSERT INTO users (` +
`id, name, email, picture, completed, created_at` + `id, name, email, picture, grade_level, teacher, admin, created_at` +
`) VALUES (` + `) VALUES (` +
`$1, $2, $3, $4, $5, $6` + `$1, $2, $3, $4, $5, $6, $7, $8` +
`)` + `)` +
` ON CONFLICT (id) DO ` + ` ON CONFLICT (id) DO ` +
`UPDATE SET ` + `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 // run
logf(sqlstr, u.ID, u.Name, u.Email, u.Picture, u.Completed, u.CreatedAt) 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.Completed, u.CreatedAt); err != nil { 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) return logerror(err)
} }
// set exists // 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) { func UserByID(ctx context.Context, db DB, id string) (*User, error) {
// query // query
const sqlstr = `SELECT ` + const sqlstr = `SELECT ` +
`id, name, email, picture, completed, created_at ` + `id, name, email, picture, grade_level, teacher, admin, created_at ` +
`FROM users ` + `FROM users ` +
`WHERE id = $1` `WHERE id = $1`
// run // run
@ -141,7 +144,7 @@ func UserByID(ctx context.Context, db DB, id string) (*User, error) {
u := User{ u := User{
_exists: true, _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 nil, logerror(err)
} }
return &u, nil 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) { func UserByEmail(ctx context.Context, db DB, email string) (*User, error) {
// query // query
const sqlstr = `SELECT ` + const sqlstr = `SELECT ` +
`id, name, email, picture, completed, created_at ` + `id, name, email, picture, grade_level, teacher, admin, created_at ` +
`FROM users ` + `FROM users ` +
`WHERE email = $1` `WHERE email = $1`
// run // run
@ -161,12 +164,39 @@ func UserByEmail(ctx context.Context, db DB, email string) (*User, error) {
u := User{ u := User{
_exists: true, _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 nil, logerror(err)
} }
return &u, nil 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. // UserByToken runs a custom query, returning results as User.
func UserByToken(ctx context.Context, db DB, token string) (*User, error) { func UserByToken(ctx context.Context, db DB, token string) (*User, error) {
// query // query
@ -178,7 +208,7 @@ func UserByToken(ctx context.Context, db DB, token string) (*User, error) {
// run // run
logf(sqlstr, token) logf(sqlstr, token)
var u User 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 nil, logerror(err)
} }
return &u, nil 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: datatype:
type: text type: text
is_primary: true 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 - type: table
name: users name: users
columns: columns:
@ -120,7 +76,14 @@ schemas:
- name: picture - name: picture
datatype: datatype:
type: text type: text
- name: completed - name: grade_level
datatype:
type: integer
nullable: true
- name: teacher
datatype:
type: boolean
- name: admin
datatype: datatype:
type: boolean type: boolean
- name: created_at - name: created_at

@ -13,6 +13,7 @@ info:
paths: paths:
/auth/code: /auth/code:
get: get:
description: Generate oauth exchange url.
tags: ["Auth"] tags: ["Auth"]
operationId: gen oauth operationId: gen oauth
parameters: parameters:
@ -24,7 +25,7 @@ paths:
format: uri format: uri
responses: responses:
'200': '200':
description: OAuth Consent Page URI description: OAuth Consent Page URI.
content: content:
application/json: application/json:
schema: schema:
@ -34,6 +35,7 @@ paths:
/auth/authorize: /auth/authorize:
get: get:
description: Authorization response callback location.
tags: ["Auth"] tags: ["Auth"]
operationId: authorize callback operationId: authorize callback
parameters: parameters:
@ -49,7 +51,7 @@ paths:
type: string type: string
responses: responses:
'302': '302':
description: Redirect to webpage description: Redirect to webpage.
headers: headers:
Location: Location:
schema: schema:
@ -71,20 +73,185 @@ paths:
default: false default: false
- name: token - name: token
in: cookie in: cookie
description: User authentication token description: User authentication token.
required: true required: true
schema: schema:
type: string type: string
responses: responses:
'204': '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: default:
$ref: "#/components/responses/DefaultResponse" $ref: "#/components/responses/DefaultResponse"
components: components:
responses: responses:
DefaultResponse: DefaultResponse:
description: Unexpected server error or invalid user input description: Unexpected server error or invalid user input.
content: content:
application/json: application/json:
schema: schema:
@ -99,6 +266,37 @@ components:
url: url:
type: string type: string
format: uri 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: Error:
type: object type: object

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